// 任务管理页面JavaScript

// 小工具：HTML 转义，避免用户输入导致的 HTML 注入
function escapeHtml(str) {
    if (str === null || str === undefined) return '';
    return String(str)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;');
}

function formatHMS(sec){
    sec = parseInt(sec) || 0;
    const h = Math.floor(sec/3600); const m = Math.floor((sec%3600)/60); const s2 = Math.floor(sec%60);
    if (h>0) return `${h}h ${m}m ${s2}s`; if (m>0) return `${m}m ${s2}s`; return `${s2}s`;
}

function formatTS(ts){ try { return new Date(ts).toLocaleString(); } catch(e){ return String(ts); } }

// Tunable rendering params: adjust for perceived performance on cold-load
const XFIO_UI_RENDER = {
    initialRenderCount: 6,
    batchSize: 4,
};

document.addEventListener('DOMContentLoaded', function() {
    loadTasks();
    updateTaskStats();
    const createModal = document.getElementById('createTaskModal');
    if (createModal) {
        try {
            createModal.addEventListener('shown.bs.modal', function() {
                try { loadTestCasesForAssignment(); } catch (e) {}
                try { loadWorkersForAssignment(); } catch (e) {}
                try { bindCreateTaskSubmit(); } catch (e) {}
                try { initCreateTaskWizard(); } catch (e) {}
            });
        } catch (e) {}
    }
});

// Delegated listener: catch change events from any .task-select checkboxes (works for dynamic rows)
document.addEventListener('change', function(e) {
    try {
        if (!e.target) return;
        if (e.target.classList && e.target.classList.contains('task-select')) {
            updateBatchDeleteState();
            updateSelectAllCheckboxForTasks();
        }
    } catch (err) { /* ignore */ }
});

// 初始化创建任务向导（在多选用例改造后被移除，这里补回）
// 注意：模板已拆分为 5 步（1: 基本信息, 2: 选择用例, 3: 配置 Worker, 4: 用例编排, 5: 预览/提交）
let wizardState = { step: 1, total: 5 };
function initCreateTaskWizard() {
    wizardState = { step: 1, total: 5 };
        // Execution plan state used when multiple test cases selected
        window.XFIO_EXEC_PLAN = window.XFIO_EXEC_PLAN || { steps: [] };
    const backBtn = document.getElementById('wizardBackBtn');
    const nextBtn = document.getElementById('wizardNextBtn');
    const submitBtn = document.getElementById('submitTaskBtn');
    if (backBtn) backBtn.onclick = () => showWizardStep(Math.max(1, wizardState.step - 1));
    if (nextBtn) nextBtn.onclick = () => showWizardStep(Math.min(wizardState.total, wizardState.step + 1));
    if (submitBtn) submitBtn.onclick = assignTask;
    // 初始：第1步为填写基本信息（任务名称），所以根据名称是否为空来决定是否启用下一步
    try {
        const nameInput = document.getElementById('taskNameInput');
        const hasName = nameInput && nameInput.value && nameInput.value.trim().length > 0;
        if (nextBtn) nextBtn.disabled = !hasName;
    } catch(e) { if (nextBtn) nextBtn.disabled = true; }

    // 绑定与第2步校验相关的输入事件（延迟绑定因为 UI 在第2步动态渲染）
    setTimeout(() => {
        // 绑定第1步：任务名称输入监听（实时启用/禁用下一步）
        const nameInput = document.getElementById('taskNameInput');
        if (nameInput && !nameInput.__wizardNameBound) {
            nameInput.__wizardNameBound = true;
            nameInput.addEventListener('input', debounce(function(){ updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e){} }, 120));
            nameInput.addEventListener('blur', function(){ this.value = this.value.trim(); updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e){} });
        }
        const globalInput = document.getElementById('globalTestPathInput');
        if (globalInput && !globalInput.__wizardBound) {
            globalInput.__wizardBound = true;
            globalInput.addEventListener('input', debounce(function(){
                const st = window.XFIO_ASSIGN_STATE; if (st && st.global) st.global.filename = globalInput.value; // 先不 trim，实时显示
                updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {}
            }, 150));
            globalInput.addEventListener('blur', function(){ this.value = this.value.trim(); const st = window.XFIO_ASSIGN_STATE; if (st && st.global) st.global.filename = this.value; updateWizardNextButtonState(); });
        }
        const overridesList = document.getElementById('workerOverridesList');
        if (overridesList && !overridesList.__wizardBound) {
            overridesList.__wizardBound = true;
            overridesList.addEventListener('input', function(e){
                if (e.target && e.target.classList.contains('ov-path')) {
                    const wid = e.target.closest('.override-row')?.dataset.workerId;
                    if (wid) {
                        const st = window.XFIO_ASSIGN_STATE; const raw = e.target.value; const val = raw.trim();
                        if (val) { if (!st.overrides[wid]) st.overrides[wid] = { filename: val }; else st.overrides[wid].filename = val; }
                        else { if (st.overrides[wid]) delete st.overrides[wid]; }
                    }
                    updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {}
                }
            });
            overridesList.addEventListener('blur', function(e){ if (e.target && e.target.classList && e.target.classList.contains('ov-path')) { e.target.value = e.target.value.trim(); updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {} }}, true);
            const mo = new MutationObserver(() => { updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {} });
            mo.observe(overridesList, { childList:true }); overridesList.__wizardObserver = mo;
        }
        const workerArea = document.getElementById('workerSelectionArea');
        if (workerArea && !workerArea.__wizardBound) {
            workerArea.__wizardBound = true;
            workerArea.addEventListener('change', function(e){ if (e.target && e.target.classList.contains('worker-checkbox')) updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {} });
        }
    }, 300);
    showWizardStep(1);
}

// 计算选中 worker 是否都拥有有效路径（覆盖或全局）
function validateAllSelectedWorkersHavePath() {
    const st = window.XFIO_ASSIGN_STATE || { global:{}, overrides:{} };
    // 清理 overrides 中的空值
    if (st.overrides) {
        Object.keys(st.overrides).forEach(k => {
            const v = st.overrides[k];
            if (!v || !v.filename || !v.filename.trim()) delete st.overrides[k];
            else v.filename = v.filename.trim();
        });
    }
    const globalPathRaw = (st.global && typeof st.global.filename === 'string') ? st.global.filename : '';
    const globalPath = globalPathRaw.trim();
    const selectedWorkers = Array.from(document.querySelectorAll('#workerSelectionArea .worker-checkbox:checked'));
    if (!selectedWorkers.length) return false; // 至少一个节点
    if (!globalPath && (!st.overrides || !Object.keys(st.overrides).length)) return false; // 没有任何路径来源
    return selectedWorkers.every(cb => {
        const wid = cb.value;
        const ov = st.overrides && st.overrides[wid];
        const ovPath = ov && ov.filename ? ov.filename.trim() : '';
        return !!(ovPath || globalPath);
    });
}

function updateWizardNextButtonState() {
    const nextBtn = document.getElementById('wizardNextBtn');
    if (!nextBtn) return;
    // 新流程：
    // step 1: 基本信息（必须填写任务名称）
    // step 2: 至少选择一个测试用例
    // step 3: 至少选择一个 worker 且每个选中 worker 有有效路径
    // step 4: 执行计划不为空（多步骤场景）
    if (wizardState.step === 1) {
        const nameInput = document.getElementById('taskNameInput');
        const hasName = nameInput && nameInput.value && nameInput.value.trim().length > 0;
        nextBtn.disabled = !hasName;
    } else if (wizardState.step === 2) {
        const hasSelected = document.querySelectorAll('#testCaseList .testcase-checkbox:checked').length > 0;
        nextBtn.disabled = !hasSelected;
    } else if (wizardState.step === 3) {
        nextBtn.disabled = !validateAllSelectedWorkersHavePath();
    }
    // 同步进度UI
    try { updateWizardProgressUI(); } catch(e) {}
}

// 合并版：更新向导进度条、标题高亮与 step 勾选（输出到 #wizardProgressLabel）
function updateWizardProgressUI() {
    const bar = document.getElementById('wizardProgress');
    const label = document.getElementById('wizardProgressLabel'); // primary detailed progress line under the bar
    const s1 = document.getElementById('stepLabel1');
    const s2 = document.getElementById('stepLabel2');
    const s3 = document.getElementById('stepLabel3');
    const s4 = document.getElementById('stepLabel4');
    const s5 = document.getElementById('stepLabel5');
    const step = wizardState.step || 1;

    // 计数基础数据
    const selectedCases = document.querySelectorAll('#testCaseList .testcase-checkbox:checked').length;
    const selectedWorkers = document.querySelectorAll('#workerSelectionArea .worker-checkbox:checked').length;
    const assignedPaths = (function(){
        const st = window.XFIO_ASSIGN_STATE || { global:{}, overrides:{} };
        const globalPath = (st.global && st.global.filename ? st.global.filename.trim() : '');
        const sel = Array.from(document.querySelectorAll('#workerSelectionArea .worker-checkbox:checked'));
        let cnt = 0;
        sel.forEach(cb => { const wid = cb.value; const ov = st.overrides && st.overrides[wid]; const ovPath = ov && ov.filename ? ov.filename.trim() : ''; if (ovPath || globalPath) cnt++; });
        return cnt;
    })();

    // 使用基于当前步骤的简单进度量度：step / total
    const planSteps = (window.XFIO_EXEC_PLAN && Array.isArray(window.XFIO_EXEC_PLAN.steps)) ? window.XFIO_EXEC_PLAN.steps.length : 0;
    const percent = Math.round(((step) / (wizardState.total || 5)) * 100);

    if (bar) {
        bar.style.width = percent + '%';
        bar.setAttribute('aria-valuenow', percent);
    }

    // 详细进度（若存在）— 根据新步骤映射展示信息
    if (label) {
        const configuredWorkers = Object.keys((window.XFIO_ASSIGN_STATE && window.XFIO_ASSIGN_STATE.overrides) || {}).length;
        let desc = `步骤 ${step} / ${wizardState.total}`;
        if (step === 1) desc += ` — 基本信息`;
        else if (step === 2) desc += ` — 已选用例 ${selectedCases}`;
        else if (step === 3) desc += ` — 已选节点 ${selectedWorkers}`;
        else if (step === 4) desc += ` — 用例编排 ${planSteps}`;
        else if (step === 5) desc += ` — 准备提交: 用例 ${selectedCases}，节点 ${selectedWorkers}`;
        label.textContent = '进度: ' + desc;
    }

    // 详细进度文本已经写入上方 label（若存在），不再维护额外的短行文案

    // 标题高亮与勾选（支持 5 步）
    [s1, s2, s3, s4, s5].forEach((el, idx) => {
        if (!el) return;
        const idx1 = idx + 1;
        el.classList.toggle('text-primary', idx1 === step);
        el.classList.toggle('fw-semibold', idx1 === step);
        el.classList.toggle('text-muted', idx1 !== step);
        const chk = el.querySelector('.step-check');
        // mark steps up to current step as done
        if (chk) chk.classList.toggle('done', (idx1 <= step));
    });
}

// 加载任务列表（恢复被误删的实现）
function loadTasks() {
    fetch('/api/tasks')
        .then(r => r.json())
        .then(data => {
            let tasks = data;
            if (tasks === null) tasks = [];
            else if (!Array.isArray(tasks)) tasks = (data && Array.isArray(data.tasks)) ? data.tasks : [];
            const tbody = document.querySelector('#tasksTable tbody');
            if (!tbody) return;
            tbody.innerHTML = '';
            if (!tasks.length) {
                const tr = document.createElement('tr');
                tr.innerHTML = '<td colspan="7" class="text-center">暂无任务数据</td>';
                tbody.appendChild(tr); return;
            }
            tasks.forEach(task => {
                const tr = document.createElement('tr');
                const createdAt = task.created_at ? new Date(task.created_at).toLocaleString() : 'N/A';
                const startTime = task.start_time ? new Date(task.start_time).toLocaleString() : 'N/A';
                const endTime = task.end_time ? new Date(task.end_time).toLocaleString() : 'N/A';
                const statusClass = task.status === 'completed' ? 'success' : (task.status === 'running' ? 'warning' : (task.status === 'pending' ? 'info' : 'secondary'));
                // 完成状态的任务也可以编辑/删除/查看（与其它状态一致），但运行相关操作根据状态不同
                const canEdit = task.status !== 'running';
                // 允许对 pending 或已完成的任务重新开始执行
                const canStart = task.status === 'pending' || task.status === 'completed';
                const canStop = task.status === 'running';
                const canViewResult = task.status === 'completed';
                let actionButtons = `
                    <button class="btn btn-sm btn-info view-btn" data-id="${task.id}">查看</button>
                    <button class="btn btn-sm btn-danger delete-btn" data-id="${task.id}">删除</button>`;
                if (canEdit) {
                    actionButtons += ` <button class="btn btn-sm btn-warning update-btn" data-id="${task.id}">编辑</button>`;
                }
                if (canStart) {
                    actionButtons += ` <button class="btn btn-sm btn-success start-btn" data-id="${task.id}">开始</button>`;
                }
                if (canStop) {
                    // 停止按钮仅在运行中显示
                    actionButtons += ` <button class="btn btn-sm btn-danger stop-btn" data-id="${task.id}">停止</button>`;
                }
                tr.innerHTML = `
                    <td><input type="checkbox" class="task-select" data-id="${task.id}"></td>
                    <td>${task.name || task.test_case_id || 'N/A'}</td>
                    <td><span class="badge bg-${statusClass}">${task.status}</span></td>
                    <td>${createdAt}</td>
                    <td>${startTime}</td>
                    <td>${endTime}</td>
                    <td>${actionButtons}</td>`;
                tbody.appendChild(tr);
            });
                bindTaskOperationEvents();
                // 确保批量删除按钮状态与选择一致
                updateBatchDeleteState();
                // 绑定单个复选框 change 事件以动态更新状态
                document.querySelectorAll('#tasksTable tbody .task-select').forEach(cb => {
                    cb.addEventListener('change', function(e) { updateBatchDeleteState(); updateSelectAllCheckboxForTasks(); });
                });
        })
        .catch(err => {
            const tbody = document.querySelector('#tasksTable tbody');
            if (tbody) tbody.innerHTML = `<tr><td colspan="7" class="text-center text-danger">加载任务数据失败: ${err.message}</td></tr>`;
            updateBatchDeleteState();
        });
}

// 更新批量删除按钮的可用性和样式
function updateBatchDeleteState() {
    const btn = document.getElementById('batchDeleteBtn');
    if (!btn) return;
    const any = document.querySelectorAll('#tasksTable tbody .task-select:checked').length > 0;
    btn.disabled = !any;
    if (any) {
        btn.classList.remove('btn-outline-danger');
        btn.classList.add('btn-danger');
    } else {
        btn.classList.remove('btn-danger');
        btn.classList.add('btn-outline-danger');
    }
    // keep the header selectAll checkbox in sync
    try { updateSelectAllCheckboxForTasks(); } catch(e) {}
}

// Synchronize the header #selectAll checkbox with the row selections
function updateSelectAllCheckboxForTasks() {
    const header = document.getElementById('selectAll');
    if (!header) return;
    const all = Array.from(document.querySelectorAll('#tasksTable tbody .task-select'));
    if (!all.length) {
        header.checked = false; header.indeterminate = false; return;
    }
    const checkedCount = all.filter(cb => cb.checked).length;
    if (checkedCount === 0) { header.checked = false; header.indeterminate = false; }
    else if (checkedCount === all.length) { header.checked = true; header.indeterminate = false; }
    else { header.checked = false; header.indeterminate = true; }
}

function showWizardStep(step) {
    const prev = wizardState.step;
    // 边界钳制
    if (step < 1) step = 1; if (step > wizardState.total) step = wizardState.total;
    // 按新步骤顺序做跨步校验：
    // 1 -> 2: 基本信息（任务名）必须存在
    if (prev === 1 && step === 2) {
        const nameInput = document.getElementById('taskNameInput');
        const hasName = nameInput && nameInput.value && nameInput.value.trim().length > 0;
        if (!hasName) { showNotification('请填写任务名称', 'error'); return; }
    }
    // 2 -> 3: 至少选择一个测试用例
    if (prev === 2 && step === 3) {
        const selectedCases = document.querySelectorAll('#testCaseList .testcase-checkbox:checked');
        if (!selectedCases.length) { showNotification('请至少选择一个测试用例', 'error'); return; }
    }
    // 3 -> 4: 至少一个 worker 且为每个选中 worker 配置路径
    if (prev === 3 && step === 4) {
        if (!validateAllSelectedWorkersHavePath()) { showNotification('请先选择至少一个节点并为其配置测试路径（全局或单独覆盖）', 'error'); return; }
    }
    // 4 -> 5: 确保有执行计划（多用例场景）
    if (prev === 4 && step === 5) {
        const steps = (window.XFIO_EXEC_PLAN && Array.isArray(window.XFIO_EXEC_PLAN.steps)) ? window.XFIO_EXEC_PLAN.steps : [];
        if (!steps.length) { showNotification('当前没有要执行的用例，请返回选择用例并编排执行顺序', 'error'); return; }
    }
    wizardState.step = step;
    for (let i = 1; i <= wizardState.total; i++) {
        const el = document.getElementById(`wizardStep${i}`);
        if (!el) continue;
        if (i === step) el.classList.remove('d-none'); else el.classList.add('d-none');
    }
    // 按钮状态
    const backBtn = document.getElementById('wizardBackBtn');
    const nextBtn = document.getElementById('wizardNextBtn');
    const submitBtn = document.getElementById('submitTaskBtn');
    if (backBtn) backBtn.disabled = (step === 1);
    if (nextBtn) nextBtn.classList.toggle('d-none', step === wizardState.total);
    if (submitBtn) submitBtn.classList.toggle('d-none', step !== wizardState.total);
    if (nextBtn) updateWizardNextButtonState();
    // 同步进度条与文案
    try { updateWizardProgressUI(); } catch(e) {}
    // 进入预览页刷新
    if (step === wizardState.total) {
        // 在进入最后一步（提交预览）时渲染预览
        updateWizardStep3Title();
        renderAssignmentPreview();
    }
}

function updateWizardStep3Title() {
    // 为兼容旧逻辑，确保用例编排标签保持为模版中第4项的文本
    const stepLabel4 = document.getElementById('stepLabel4');
    if (stepLabel4) stepLabel4.textContent = '4. 用例编排';
}

// 第4步预览：生成任务提交前的概览（用例 / 节点 / 执行计划）
function renderAssignmentPreview() {
    const preview = document.getElementById('assignmentPreview');
    if (!preview) return;
    const checkedCases = Array.from(document.querySelectorAll('#testCaseList .testcase-checkbox:checked'));
    const caseCount = checkedCases.length;
    const caseNames = checkedCases.slice(0,5).map(cb => {
        const card = cb.closest('.testcase-card');
        return card ? (card.querySelector('h6')?.textContent.trim() || cb.value) : cb.value;
    });
    const workers = Array.from(document.querySelectorAll('#workerSelectionArea .worker-card .worker-checkbox:checked'));
    const workerCount = workers.length;
    const workerNames = workers.slice(0,8).map(cb => {
        const card = cb.closest('.worker-card');
        return card ? (card.querySelector('.wk-name')?.textContent.trim() || cb.value) : cb.value;
    });
    const plan = (window.XFIO_EXEC_PLAN && Array.isArray(window.XFIO_EXEC_PLAN.steps)) ? window.XFIO_EXEC_PLAN.steps : [];
    const multiPlan = plan.length > 1;
    let planSummary = '';
    if (multiPlan) {
        planSummary = plan.map(s => `#${s.step}:${s.name||s.test_case_id}(延迟${s.delay_after||0}s)`).join(' → ');
        if (planSummary.length > 220) planSummary = planSummary.slice(0,220) + '…';
    }
        // 构建更友好的结构化预览：用例列表、节点表（包含路径）、执行计划（有序）
        let casesHtml = '';
        if (caseCount) {
                casesHtml = '<ol class="mb-1 small ps-3">';
                checkedCases.forEach(cb => {
                        const tcid = cb.value;
                        const tcObj = (window.XFIO_TESTCASES || []).find(t => String(t.id) === String(tcid) || String(t.test_case_id) === String(tcid));
                        const tcName = tcObj ? (tcObj.name || tcObj.title || tcObj.test_case_id || tcid) : tcid;
                        const rw = tcObj && (tcObj.rw || tcObj.io_mode || tcObj.mode) ? (tcObj.rw || tcObj.io_mode || tcObj.mode) : '';
                        const bs = tcObj && (tcObj.bs || tcObj.block_size) ? (tcObj.bs || tcObj.block_size) : '';
                        const nj = tcObj && (tcObj.numjobs || tcObj.jobs) ? (tcObj.numjobs || tcObj.jobs) : '';
                        const iodepth = tcObj && (tcObj.iodepth || tcObj.depth) ? (tcObj.iodepth || tcObj.depth) : '';
                        let meta = '';
                        if (rw) meta += `<span class="badge bg-secondary me-1">${rw}</span>`;
                        if (nj || iodepth) meta += `<span class="badge bg-secondary me-1">W${nj}D${iodepth}</span>`;
                        if (bs) meta += `<span class="badge bg-secondary">bs:${bs}</span>`;
                        casesHtml += `<li>${tcName} ${meta}</li>`;
                });
                casesHtml += '</ol>';
        } else {
                casesHtml = '<div class="small text-muted">（无）</div>';
        }

    // 仅展示节点名称（紧凑的 badge/芯片），其余详细信息保留在 JSON 区块
        let workersHtml = '';
        if (workerCount) {
            workersHtml = '<div class="d-flex flex-wrap gap-2 align-items-start">';
            workers.forEach(cb => {
                const wid = cb.value;
                const card = cb.closest('.worker-card');
                const name = card ? (card.querySelector('.wk-name')?.textContent.trim() || wid) : wid;
                // 使用 badge 展示名称，鼠标悬停显示完整节点 ID
                workersHtml += `<span class="badge bg-outline-secondary text-body border rounded-pill py-1 px-2" title="${wid}">${escapeHtml(name)}</span>`;
            });
            workersHtml += '</div>';
        } else {
            workersHtml = '<div class="small text-muted">（无）</div>';
        }

        // 执行计划展示（简化）：只展示每步名称与该步的运行时/延时 badge，时间估算只显示整任务总时长
        let planHtml = '';
        let totalEstimatedSec = 0;
        if (plan && plan.length) {
            planHtml = '<ol class="mb-1 small ps-3">';
            plan.forEach((s, idx) => {
                const tcObj = (window.XFIO_TESTCASES || []).find(t => String(t.id) === String(s.test_case_id) || String(t.test_case_id) === String(s.test_case_id));
                const tcName = tcObj ? (tcObj.name || tcObj.title || tcObj.test_case_id || s.test_case_id) : (s.name || s.test_case_id || `#${s.test_case_id}`);
                const delay = (s.delay_after || 0);
                // 估算 runtime（优先级：步骤 params.runtime -> 步骤.runtime -> 用例 fio_config.runtime -> 默认 180s）
                let stepRuntime = 180;
                if (s.params && s.params.runtime) stepRuntime = parseInt(s.params.runtime) || stepRuntime;
                else if (s.runtime) stepRuntime = parseInt(s.runtime) || stepRuntime;
                else if (tcObj && tcObj.fio_config && (tcObj.fio_config.runtime !== undefined && tcObj.fio_config.runtime !== null)) stepRuntime = parseInt(tcObj.fio_config.runtime) || stepRuntime;
                totalEstimatedSec += stepRuntime + (delay || 0);
                // 显示简洁的 badge：运行时与延迟
                let meta = '';
                if (stepRuntime) meta += `<span class="badge bg-secondary me-1">${stepRuntime}s</span>`;
                if (delay) meta += `<span class="badge bg-warning text-dark">延迟 ${delay}s</span>`;
                planHtml += `<li><strong>${escapeHtml(tcName)}</strong> ${meta}</li>`;
            });
            planHtml += '</ol>';
        } else {
            planHtml = '<div class="small text-muted">（无自定义编排，使用单用例/默认执行）</div>';
        }

            const html = `
                    <div class="card border-secondary mb-2 shadow-sm">
                        <div class="card-body p-3">
                            <div class="d-flex justify-content-between align-items-center mb-2">
                                <div class="small text-muted">提交前概览</div>
                                <div class="small text-muted">用例 ${caseCount} • 节点 ${workerCount} • 步骤 ${plan.length}</div>
                            </div>
                            <div class="row gx-3 gy-2">
                                <div class="col-12 col-md-4">
                                    <div class="small mb-1 text-muted"><strong>用例</strong></div>
                                    <div class="bg-white border rounded p-2" style="min-height:80px;">
                                        ${casesHtml}
                                    </div>
                                </div>
                                <div class="col-12 col-md-4">
                                    <div class="small mb-1 text-muted"><strong>执行计划</strong></div>
                                    <div class="bg-white border rounded p-2" style="min-height:80px;">
                                        ${planHtml}
                                    </div>
                                </div>
                                <div class="col-12 col-md-4">
                                    <div class="small mb-1 text-muted"><strong>节点</strong></div>
                                    <div class="bg-white border rounded p-2 d-flex align-items-center" style="min-height:80px;">
                                        ${workersHtml}
                                    </div>
                                </div>
                            </div>
                                        <div class="mt-2 small text-muted">预计总时长: ${formatHMS(totalEstimatedSec)}；预计完成: ${formatTS(Date.now() + totalEstimatedSec*1000)}</div>
                                        <details class="small mt-3">
                                <summary class="text-primary" style="cursor:pointer;">展开查看 JSON 预览</summary>
                                <pre class="bg-light border rounded p-2 mt-1 mb-0" style="max-height:320px;overflow:auto;font-size:0.75rem;" id="assignmentPreviewJson"></pre>
                            </details>
                        </div>
                    </div>`;
        preview.innerHTML = html;
    // 构造与 assignTask 同逻辑的草稿 JSON 以供查看（不触发校验/提交）
    try {
        const st = window.XFIO_ASSIGN_STATE || { global:{}, overrides:{} };
        const globalPath = (st.global && st.global.filename ? st.global.filename.trim() : '');
        const workersDraft = workers.map((cb, idx) => {
            const wid = cb.value; const ov = st.overrides[wid] || {}; const filename = (ov.filename||'').trim() || globalPath; const overrideObj = filename ? { filename, _sequence: idx+1 } : { _sequence: idx+1 }; return { worker_id: wid, task_config_override: filename ? overrideObj : null };
        });
        let draft = { test_case_id: checkedCases[0]?checkedCases[0].value:'', workers: workersDraft, worker_ids: workersDraft.map(w=>w.worker_id) };
        if (multiPlan) {
            const sanitized = plan.map((s, idx) => ({ step: idx+1, test_case_id: s.test_case_id, name: s.name||'', workers: workersDraft.map(w=>w.worker_id), overrides: null, params: {}, run_mode: s.run_mode||'auto', delay_after: s.delay_after||0 }));
            draft.execution_plan = sanitized; draft.plan_name = 'preview-plan';
        }
        const pre = document.getElementById('assignmentPreviewJson'); if (pre) pre.textContent = JSON.stringify(draft, null, 2);
    } catch(e) { console.warn('preview json build failed', e); }
}

// 绑定任务操作按钮事件
function bindTaskOperationEvents() {
    // 开始任务按钮事件
    document.querySelectorAll('.start-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            startTask(taskId);
        });
    });
    
    // 查看结果按钮事件
    document.querySelectorAll('.view-result-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            viewTaskResult(taskId);
        });
    });

    // 查看详情按钮事件
    document.querySelectorAll('.view-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            viewTaskDetail(taskId);
        });
    });

    // 删除任务按钮事件
    document.querySelectorAll('.delete-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            deleteTask(taskId);
        });
    });

    // 停止任务按钮事件
    document.querySelectorAll('.stop-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            stopTask(taskId);
        });
    });

    // 编辑任务按钮事件
    document.querySelectorAll('.update-btn').forEach(button => {
        button.addEventListener('click', function() {
            const taskId = this.getAttribute('data-id');
            editTask(taskId);
        });
    });
}

// 批量选择：全选/反选
document.addEventListener('click', function(e) {
    if (!e.target) return;
    if (e.target.id === 'selectAll') {
        const checked = e.target.checked;
        document.querySelectorAll('#tasksTable tbody .task-select').forEach(cb => {
            cb.checked = checked;
            // 触发 change 事件以保证其他监听器（如 updateBatchDeleteState）执行
            try { cb.dispatchEvent(new Event('change', { bubbles: true })); } catch(e) {}
        });
        // 额外更新批量删除按钮状态以防丢失
        try { updateBatchDeleteState(); } catch(e) {}
    }
});

// 获取选中的任务ID数组
function getSelectedTaskIds() {
    return Array.from(document.querySelectorAll('#tasksTable tbody .task-select:checked')).map(cb => cb.getAttribute('data-id'));
}

// 绑定页面级按钮（批量删除）
document.addEventListener('DOMContentLoaded', function() {
    const batchBtn = document.getElementById('batchDeleteBtn');
    if (batchBtn) {
        batchBtn.addEventListener('click', function() {
            const ids = getSelectedTaskIds();
            if (!ids.length) { showNotification('请先选择要删除的任务', 'error'); return; }
            if (!confirm(`确认删除 ${ids.length} 个任务？此操作不可撤销。`)) return;
            fetch('/api/tasks/batch-delete', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ ids })
            })
            .then(async r => {
                const txt = await r.text();
                try { return { statusCode: r.status, body: JSON.parse(txt || '{}') }; }
                catch(e) { return { statusCode: r.status, body: txt }; }
            })
            .then(resp => {
                if (resp && resp.body && (resp.body.status === 'success' || resp.body.deleted_count > 0)) {
                    showNotification(`已删除 ${resp.body.deleted_count || ids.length} 个任务`, 'success');
                    loadTasks(); updateTaskStats();
                } else {
                    console.error('批量删除失败，响应：', resp);
                    showNotification('批量删除失败: ' + (typeof resp.body === 'string' ? resp.body : (resp.body && resp.body.error) || '未知错误'), 'error');
                }
            })
            .catch(err => {
                console.error('批量删除错误', err);
                showNotification('批量删除出错: ' + err.message, 'error');
            });
        });
    }
});

// 开始任务
function startTask(taskId) {
    fetch(`/api/tasks/${taskId}/start`, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        }
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'started') {
            showNotification('任务已开始', 'success');
            loadTasks(); // 刷新任务列表
        } else {
            showNotification('启动任务失败: ' + (data.error || '未知错误'), 'error');
        }
    })
    .catch(error => {
        console.error('Error starting task:', error);
        showNotification('启动任务时发生错误: ' + error.message, 'error');
    });
}

// 停止任务
function stopTask(taskId) {
    fetch(`/api/tasks/${taskId}/stop`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
    })
    .then(response => response.json())
    .then(data => {
        if (data.status === 'stopped' || data.success) {
            showNotification('任务已停止', 'success');
            loadTasks();
        } else {
            showNotification('停止任务失败: ' + (data.error || '未知错误'), 'error');
        }
    })
    .catch(error => {
        console.error('Error stopping task:', error);
        showNotification('停止任务时发生错误: ' + error.message, 'error');
    });
}

// 查看任务结果
function viewTaskResult(taskId) {
    fetch(`/api/tasks/${taskId}`)
        .then(response => response.json())
        .then(task => {
            // 创建结果预览模态框
            createResultModal(task);
        })
        .catch(error => {
            console.error('Error fetching task result:', error);
            showNotification('获取任务结果时发生错误: ' + error.message, 'error');
        });
}

// 创建结果预览模态框
function createResultModal(task) {
    // 创建模态框元素
    const modalBackdrop = document.createElement('div');
    modalBackdrop.className = 'modal-backdrop fade show';
    modalBackdrop.style.zIndex = '1050';
    
    const modal = document.createElement('div');
    modal.className = 'modal fade show';
    modal.style.display = 'block';
    modal.style.zIndex = '1051';
    modal.innerHTML = `
        <div class="modal-dialog modal-xl">
            <div class="modal-content">
                <div class="modal-header bg-info text-white">
                    <h5 class="modal-title">任务结果 - ${task.id}</h5>
                    <button type="button" class="btn-close btn-close-white close-result-modal" aria-label="Close"></button>
                </div>
                <div class="modal-body">
                    <div class="row">
                        <div class="col-md-6">
                            <h6>任务信息</h6>
                            <table class="table table-sm">
                                <tr><td>任务ID</td><td>${task.id}</td></tr>
                                <tr><td>测试用例ID</td><td>${task.test_case_id}</td></tr>
                                <tr><td>Worker ID</td><td>${task.worker_id}</td></tr>
                                <tr><td>状态</td><td><span class="badge bg-${task.status === 'completed' ? 'success' : task.status === 'running' ? 'warning' : 'info'}">${task.status}</span></td></tr>
                                <tr><td>创建时间</td><td>${task.created_at ? new Date(task.created_at).toLocaleString() : 'N/A'}</td></tr>
                                <tr><td>开始时间</td><td>${task.start_time ? new Date(task.start_time).toLocaleString() : 'N/A'}</td></tr>
                                <tr><td>结束时间</td><td>${task.end_time ? new Date(task.end_time).toLocaleString() : 'N/A'}</td></tr>
                            </table>
                        </div>
                        <div class="col-md-6">
                            <h6>任务配置覆盖</h6>
                            <pre class="bg-light p-2 rounded" id="taskConfigOverridePreview">${task.task_config_override ? JSON.stringify(task.task_config_override, null, 2) : '无配置覆盖'}</pre>
                        </div>
                    </div>
                    <div class="mt-3">
                        <h6>执行结果</h6>
                        <pre class="bg-light p-2 rounded" id="taskResultPreview">${task.result ? JSON.stringify(task.result, null, 2) : '暂无结果数据'}</pre>
                    </div>
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-secondary close-result-modal">关闭</button>
                </div>
            </div>
        </div>
    `;
    
    // 添加到页面
    document.body.appendChild(modal);
    document.body.appendChild(modalBackdrop);
    
    // 绑定关闭事件
    document.querySelectorAll('.close-result-modal').forEach(btn => {
        btn.addEventListener('click', function() {
            document.body.removeChild(modal);
            document.body.removeChild(modalBackdrop);
            document.body.classList.remove('modal-open');
        });
    });
    
    // 添加ESC键关闭功能
    document.addEventListener('keydown', function escHandler(e) {
        if (e.key === 'Escape') {
            document.body.removeChild(modal);
            document.body.removeChild(modalBackdrop);
            document.body.classList.remove('modal-open');
            document.removeEventListener('keydown', escHandler);
        }
    });
}

// 查看任务详情
function viewTaskDetail(taskId) {
    fetch(`/api/tasks/${taskId}`)
        .then(response => response.json())
        .then(task => {
            // 填充模态框内容
            document.getElementById('taskDetailId').textContent = task.id || 'N/A';
            document.getElementById('taskDetailName').textContent = task.name || 'N/A';
            document.getElementById('taskDetailTestCaseId').textContent = task.test_case_id || 'N/A';
            document.getElementById('taskDetailWorkerId').textContent = task.worker_id || 'N/A';
            const statusClass = task.status === 'completed' ? 'success' : (task.status === 'running' ? 'warning' : (task.status === 'pending' ? 'info' : 'secondary'));
            document.getElementById('taskDetailStatus').innerHTML = `<span class="badge bg-${statusClass}">${task.status || 'N/A'}</span>`;
            document.getElementById('taskDetailCreatedAt').textContent = task.created_at ? new Date(task.created_at).toLocaleString() : 'N/A';
            document.getElementById('taskDetailStartTime').textContent = task.start_time ? new Date(task.start_time).toLocaleString() : 'N/A';
            document.getElementById('taskDetailEndTime').textContent = task.end_time ? new Date(task.end_time).toLocaleString() : 'N/A';
            document.getElementById('taskDetailConfigOverride').textContent = task.task_config_override ? JSON.stringify(task.task_config_override, null, 2) : '无配置覆盖';
            
            const resultSection = document.getElementById('taskDetailResultSection');
            const resultPre = document.getElementById('taskDetailResult');
            if (task.result) {
                resultPre.textContent = JSON.stringify(task.result, null, 2);
                resultSection.style.display = 'block';
            } else {
                resultSection.style.display = 'none';
            }
            
            // 显示模态框
            const modal = new bootstrap.Modal(document.getElementById('viewTaskModal'));
            modal.show();
        })
        .catch(error => {
            console.error('Error fetching task detail:', error);
            showNotification('获取任务详情时发生错误: ' + error.message, 'error');
        });
}

// 删除任务
function deleteTask(taskId) {
    if (!confirm('确定要删除这个任务吗？此操作不可撤销。')) {
        return;
    }
    
    fetch(`/api/tasks/${taskId}`, {
        method: 'DELETE'
    })
    .then(response => {
        if (response.ok) {
            showNotification('任务已删除', 'success');
            loadTasks(); // 刷新任务列表
            updateTaskStats(); // 更新统计
        } else {
            return response.json().then(data => {
                throw new Error(data.error || '删除失败');
            });
        }
    })
    .catch(error => {
        console.error('Error deleting task:', error);
        showNotification('删除任务时发生错误: ' + error.message, 'error');
    });
}

// 编辑任务（暂未实现）
function editTask(taskId) {
    showNotification('编辑任务功能暂未实现', 'info');
    // TODO: 实现编辑功能，可能需要重新打开创建任务模态框并预填充数据
}

// 更新任务统计
function updateTaskStats() {
    fetch('/api/tasks')
        .then(response => response.json())
        .then(data => {
            // 处理API返回null的情况
            let tasks = data;
            if (tasks === null) {
                tasks = [];
            } else if (!Array.isArray(tasks)) {
                // 如果返回的是对象格式（在某些实现中可能包含元数据），则尝试获取tasks数组
                tasks = (typeof data === 'object' && data !== null && Array.isArray(data.tasks)) ? data.tasks : [];
            }
            
            const totalTasks = tasks.length;
            let pendingCount = 0;
            let runningCount = 0;
            let completedCount = 0;
            
            tasks.forEach(task => {
                switch(task.status) {
                    case 'pending':
                        pendingCount++;
                        break;
                    case 'running':
                        runningCount++;
                        break;
                    case 'completed':
                        completedCount++;
                        break;
                }
            });
            
            document.getElementById('totalTasks').textContent = totalTasks;
            document.getElementById('pendingTasks').textContent = pendingCount;
            document.getElementById('runningTasks').textContent = runningCount;
            document.getElementById('completedTasks').textContent = completedCount;
        })
        .catch(error => {
            console.error('Error updating stats:', error);
            document.getElementById('totalTasks').textContent = 'Error';
            document.getElementById('pendingTasks').textContent = 'Error';
            document.getElementById('runningTasks').textContent = 'Error';
            document.getElementById('completedTasks').textContent = 'Error';
        });
}

// 加载测试用例用于任务分配
function loadTestCasesForAssignment() {
    // clean any previous lazy-loader observer/sentinel from earlier runs to avoid duplicate rendering
        try {
        if (window.__xfio_testcase_observer) {
            try { window.__xfio_testcase_observer.disconnect(); } catch (e) {}
            window.__xfio_testcase_observer = null;
        }
        const prev = document.getElementById('testcase-sentinel');
        if (prev && prev.parentNode) prev.parentNode.removeChild(prev);
    } catch (e) {}
    // mark loading to avoid concurrent calls
    window.__xfio_loading_testcases = true;

    fetch('/api/test-cases')
        .then(response => response.json())
        .then(testCases => {
            // 我们将使用卡片网格渲染 test cases，同时保留隐藏的 select/input 以兼容提交逻辑
            const select = document.getElementById('testCaseSelect');
            const list = document.getElementById('testCaseList');
            if (select) select.value = '';
            if (list) list.innerHTML = '';

            // 将 testCases 缓存到一个全局变量，供其它逻辑使用（例如 preview）
            window.XFIO_TESTCASES = Array.isArray(testCases) ? testCases : [];

            // 渲染卡片（改为按块渲染：初始少量，后续按需加载以降低首屏阻塞）
            // Timing: measure how long it takes to build and append the test-case card DOM
            let __xfio_render_start = null;
            try { if (window.performance && performance.now) __xfio_render_start = performance.now(); else __xfio_render_start = Date.now(); } catch (e) { __xfio_render_start = Date.now(); }
            try { console.time && console.time('renderTestCases'); } catch (e) {}

            // 过滤器状态（默认：全部） - 提前声明，避免在 rAF 回调中访问到未初始化的变量导致错误
            const testCaseFilters = {
                rw: 'all', // read/write/randwrite/all
                bs: 'all',
                iodepth: 'all',
                numjobs: 'all',
                q: ''
            };

            // 绑定测试用例搜索（应在测试用例加载后立即可用，而不是等待 Worker 部分）
            (function bindTestCaseSearch(){
                const input = document.getElementById('testCaseSearch');
                const btn = document.getElementById('testCaseSearchBtn');
                if (!input) return;
                if (input.__xfio_bound) return; // 避免重复绑定
                input.__xfio_bound = true;
                let timer = null;
                const trigger = () => { testCaseFilters.q = input.value || ''; applyTestCaseFilters(); };
                input.addEventListener('input', function(){
                    if (timer) clearTimeout(timer);
                    timer = setTimeout(trigger, 160);
                });
                input.addEventListener('keydown', function(e){
                    if (e.key === 'Enter') {
                        if (timer) clearTimeout(timer);
                        trigger();
                        e.preventDefault();
                    }
                });
                if (btn && !btn.__xfio_bound) {
                    btn.__xfio_bound = true;
                    btn.addEventListener('click', function(){ trigger(); });
                }
            })();

            // search input removed from UI; keep an accessible count badge if present
            const countBadge = document.getElementById('testCaseCountBadge');
            // show total count (do not overwrite later with visible count)
            if (countBadge) countBadge.textContent = XFIO_TESTCASES.length;
            // Batch render cards to avoid long main-thread blocks
            (function batchRender(cardsData) {
                if (!list) return;
                // Sort by heat: prefer explicit usage_count/hits, fall back to created_at (newer first)
                try {
                    cardsData = cardsData.slice();
                    cardsData.sort(function(a,b) {
                        const aHot = (a.usage_count || a.hits || 0);
                        const bHot = (b.usage_count || b.hits || 0);
                        if (aHot !== bHot) return bHot - aHot;
                        const at = a.created_at ? Date.parse(a.created_at) : 0;
                        const bt = b.created_at ? Date.parse(b.created_at) : 0;
                        return bt - at;
                    });
                } catch (e) {}
                const fragment = document.createDocumentFragment();
                const batchSize = (XFIO_UI_RENDER && XFIO_UI_RENDER.batchSize) ? XFIO_UI_RENDER.batchSize : 6;
                const initialRenderCount = (XFIO_UI_RENDER && XFIO_UI_RENDER.initialRenderCount) ? XFIO_UI_RENDER.initialRenderCount : 12;
                let pointer = 0;
                // rendering pointer for batch rendering

                function renderOne(tc) {
                    const card = document.createElement('div');
                    card.className = 'testcase-card card mb-2';
                    card.setAttribute('role', 'listitem');
                    card.setAttribute('tabindex', '0');
                    card.dataset.id = tc.id;
                    card.dataset.config = JSON.stringify(tc.fio_config || {});
                    const fioCfg = tc.fio_config || {};
                    const rawRw = fioCfg.rw || fioCfg['--rw'] || fioCfg['rw_mode'] || null;
                    // Present IO mode as Capitalized (first letter upper, rest lower)
                    const rwLabel = rawRw ? (String(rawRw).toString().length ? (String(rawRw).charAt(0).toUpperCase() + String(rawRw).slice(1).toLowerCase()) : String(rawRw)) : 'N/A';
                    let bsLabel = fioCfg.bs || fioCfg['block_size'] || fioCfg['bs'] || 'N/A';
                    try { bsLabel = String(bsLabel).replace(/([a-zA-Z]+)$/, function(m) { return m.toUpperCase(); }); } catch (e) {}
                    const iodepthRaw = (fioCfg.iodepth !== undefined && fioCfg.iodepth !== null) ? fioCfg.iodepth :
                                       (fioCfg['iodepth'] !== undefined && fioCfg['iodepth'] !== null) ? fioCfg['iodepth'] :
                                       (fioCfg['depth'] !== undefined && fioCfg['depth'] !== null) ? fioCfg['depth'] : null;
                    const numjobsRaw = (fioCfg.numjobs !== undefined && fioCfg.numjobs !== null) ? fioCfg.numjobs :
                                       (fioCfg['num_jobs'] !== undefined && fioCfg['num_jobs'] !== null) ? fioCfg['num_jobs'] :
                                       (fioCfg['jobs'] !== undefined && fioCfg['jobs'] !== null) ? fioCfg['jobs'] : null;
                    const iodepth = (iodepthRaw !== null && iodepthRaw !== '') ? iodepthRaw : 1;
                    const numjobs = (numjobsRaw !== null && numjobsRaw !== '') ? numjobsRaw : 1;
                    const shortTooltip = `iodepth: ${iodepth}${iodepthRaw === null || iodepthRaw === '' ? ' (default)' : ''}, numjobs: ${numjobs}${numjobsRaw == null || numjobsRaw === '' ? ' (default)' : ''}`;

                    card.innerHTML = '<div class="card-body d-flex justify-content-between align-items-start">' +
                        '<div>' +
                            `<h6 class="mb-1">${tc.name}</h6>` +
                            '<div class="mt-1">' +
                                `<span class="badge-status badge bg-transparent">${rwLabel}</span>` +
                                `<span class="badge bg-light text-dark ms-2 badge-param" style="padding:0.18rem 0.4rem">${bsLabel}</span>` +
                                `<span class="badge bg-secondary text-white ms-2 badge-param" title="${shortTooltip}" style="padding:0.18rem 0.4rem">W${numjobs}D${iodepth}</span>` +
                            '</div>' +
                        '</div>' +
                        '<div class="text-end"><div class="form-check">' +
                            `<input class="form-check-input testcase-checkbox" type="checkbox" id="testcase-${tc.id}" value="${tc.id}" aria-label="选择用例 ${tc.name}">` +
                            `<label class="form-check-label visually-hidden" for="testcase-${tc.id}">选择</label>` +
                        '</div></div>' +
                    '</div>';
                    const mark = document.createElement('div');
                    mark.className = 'select-mark';
                    mark.innerHTML = '<i class="fas fa-check"></i>';
                    card.appendChild(mark);

                    // cache a lowercase searchable string to avoid expensive card.innerText reads during filtering
                    try {
                        const wd = `W${numjobs}D${iodepth}`;
                        card.dataset.search = (String(tc.name || '') + ' ' + String(rwLabel || '') + ' ' + String(bsLabel || '') + ' ' + wd).toLowerCase();
                    } catch (e) { card.dataset.search = (tc.name || '').toLowerCase(); }

                    return card;
                }

                function appendFragment(frag) {
                    if (frag && frag.childNodes && frag.childNodes.length) list.appendChild(frag);
                }

                // render next chunk of cards (synchronous, but small)
                function renderChunk(count) {
                    const frag = document.createDocumentFragment();
                    let rendered = 0;
                    while (rendered < count && pointer < cardsData.length) {
                        const tc = cardsData[pointer++];
                        const card = renderOne(tc);
                        frag.appendChild(card);
                        rendered++;
                    }
                    appendFragment(frag);
                    // keep sentinel (if any) always as last child so observer can continue to fire
                    try {
                        const s = document.getElementById('testcase-sentinel');
                        if (s && s.parentNode === list) {
                            // move to end if not already last
                            if (list.lastElementChild !== s) list.appendChild(s);
                        }
                    } catch (e) {}
                    // apply current filters to freshly added items
                    try { applyTestCaseFilters(); } catch (e) {}
                    // keep the count badge showing total available test-cases
                    try { if (countBadge) countBadge.textContent = XFIO_TESTCASES.length; } catch (e) {}
                    return pointer >= cardsData.length;
                }

                // After initial render we attach an IntersectionObserver to load more chunks lazily
                let observer = null;
                function setupLazyLoader() {
                    // sentinel to observe near-bottom
                    let sentinel = document.getElementById('testcase-sentinel');
                    if (!sentinel) {
                        sentinel = document.createElement('div');
                        sentinel.id = 'testcase-sentinel';
                        // ensure sentinel does not become a visible grid item that breaks layout
                        sentinel.setAttribute('aria-hidden', 'true');
                            sentinel.style.cssText = 'display:block; width:100%; height:1px; visibility:hidden; pointer-events:none;';
                        // append sentinel inside the scrollable list so IntersectionObserver with root=list works
                        list.appendChild(sentinel);
                    }

                    if ('IntersectionObserver' in window) {
                        observer = new IntersectionObserver((entries) => {
                            entries.forEach(entry => {
                                if (entry.isIntersecting) {
                                    // render next chunk using rAF to avoid blocking a single frame
                                    window.requestAnimationFrame(() => {
                                        let done = false;
                                        // Loop: keep appending while not finished AND list还没有出现需要滚动的高度（避免只加载一批就停）
                                        do {
                                            done = renderChunk(batchSize);
                                            // 如果列表还不需要滚动（高度不足以溢出），继续加载下一批，直到需要滚动或全部完成
                                        } while(!done && list.scrollHeight <= list.clientHeight + 24);

                                        if (done && observer) {
                                            try { observer.disconnect(); } catch (e) {}
                                            try { sentinel.remove(); } catch (e) {}
                                            // clear global observer ref
                                            try { if (window.__xfio_testcase_observer) { window.__xfio_testcase_observer.disconnect(); window.__xfio_testcase_observer = null; } } catch (e) {}
                                            // mark loading finished
                                            window.__xfio_loading_testcases = false;
                                            // final timing/report
                                            try { console.timeEnd && console.timeEnd('renderTestCases'); } catch(e){}
                                            try {
                                                var __xfio_render_end = (window.performance && performance.now) ? performance.now() : Date.now();
                                                var __xfio_render_ms = Math.max(0, (__xfio_render_end - (__xfio_render_start || __xfio_render_end)));
                                                fetch('/api/debug/client-timing', {
                                                    method: 'POST',
                                                    headers: { 'Content-Type': 'application/json' },
                                                    body: JSON.stringify({ event: 'renderTestCases', count: XFIO_TESTCASES.length, duration_ms: Math.round(__xfio_render_ms), ts: new Date().toISOString(), userAgent: navigator && navigator.userAgent ? navigator.userAgent : 'unknown' })
                                                }).catch(function(e){});
                                            } catch (e) {}
                                        }
                                    });
                                }
                            });
                        }, { root: list, rootMargin: '300px', threshold: 0.01 });

                        observer.observe(sentinel);
                        // keep a reference globally so subsequent modal opens can clean it
                        window.__xfio_testcase_observer = observer;
                    } else {
                        // fallback: load remaining chunks on scroll
                        const onScroll = function() {
                            if (pointer >= cardsData.length) {
                                try { list.removeEventListener('scroll', onScroll); } catch (e) {}
                                return;
                            }
                            // if near bottom of the list, load next chunk
                            try {
                                if ((list.scrollHeight - list.scrollTop - list.clientHeight) <= 400) {
                                    window.requestAnimationFrame(() => renderChunk(batchSize));
                                }
                            } catch (e) {}
                        };
                        list.addEventListener('scroll', onScroll);
                    }
                }

                // start by rendering an initial small batch (split into a couple of frames to avoid blocking)
                const firstPass = Math.min(initialRenderCount, cardsData.length - pointer);
                // render in two rAF steps if larger than batchSize
                window.requestAnimationFrame(() => {
                    renderChunk(firstPass);
                    // if more remain, schedule lazy loader (no manual 'show more' control)
                    if (pointer < cardsData.length) {
                        try { setupLazyLoader(); } catch (e) { /* ignore */ }
                    }
                    // already handled lazy loading / finalizing above
                    if (pointer >= cardsData.length) {
                        // finished fast - finalize timing/report and clear loading flag
                        try { console.timeEnd && console.timeEnd('renderTestCases'); } catch(e){}
                        try {
                            var __xfio_render_end = (window.performance && performance.now) ? performance.now() : Date.now();
                            var __xfio_render_ms = Math.max(0, (__xfio_render_end - (__xfio_render_start || __xfio_render_end)));
                            fetch('/api/debug/client-timing', {
                                method: 'POST',
                                headers: { 'Content-Type': 'application/json' },
                                body: JSON.stringify({ event: 'renderTestCases', count: XFIO_TESTCASES.length, duration_ms: Math.round(__xfio_render_ms), ts: new Date().toISOString(), userAgent: navigator && navigator.userAgent ? navigator.userAgent : 'unknown' })
                            }).catch(function(e){});
                        } catch (e) {}
                        window.__xfio_loading_testcases = false;
                    }
                });

                // Event delegation: handle clicks and keydown at the container level to avoid per-card listeners
                if (list && !list.__xfio_delegation) {
                    list.__xfio_delegation = true;

                    list.addEventListener('click', function(e) {
                        // if clicked on a control, ignore
                        const ignorable = ['A','BUTTON','INPUT','SELECT','TEXTAREA','LABEL'];
                        if (e.target && e.target.tagName && ignorable.indexOf(e.target.tagName) !== -1) return;
                        // find closest card
                        const card = e.target.closest && e.target.closest('.testcase-card');
                        if (!card) return;
                        const cb = card.querySelector('.testcase-checkbox');
                        if (!cb) return;
                        // toggle multi-select
                        cb.checked = !cb.checked;
                        cb.dispatchEvent(new Event('change', { bubbles: true }));
                    });

                    list.addEventListener('keydown', function(e) {
                        if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
                            const card = e.target.closest && e.target.closest('.testcase-card');
                            if (!card) return;
                            e.preventDefault();
                            const cb = card.querySelector('.testcase-checkbox');
                            if (!cb) return;
                            cb.checked = !cb.checked;
                            cb.dispatchEvent(new Event('change', { bubbles: true }));
                        }
                    });

                    // delegate change events for checkboxes
                    list.addEventListener('change', function(e) {
                        const target = e.target;
                        if (!target || !target.classList || !target.classList.contains('testcase-checkbox')) return;
                        const cardEl = target.closest('.testcase-card');
                        if (cardEl) cardEl.classList.toggle('selected', target.checked);
                        updateMultiTestCaseSelectionUI(cardEl, target.checked);
                    });
                }
            })(XFIO_TESTCASES);

            // 多选状态更新（保留最后一个选中的详细预览，同时支持多用例汇总）
            function updateMultiTestCaseSelectionUI(lastCard, lastChecked) {
                const checked = Array.from(document.querySelectorAll('#testCaseList .testcase-checkbox:checked'));
                const label = document.getElementById('testCaseLabel');
                if (label) {
                    // visible count
                    const visibleCount = Array.from(document.querySelectorAll('#testCaseList .testcase-card')).filter(c => c.style.display !== 'none').length;
                    if (!checked.length) label.textContent = `用例：${visibleCount} 个可见，未选择`;
                    else {
                        const last = lastChecked && lastCard ? lastCard : (checked[checked.length-1].closest('.testcase-card'));
                        const lastName = last ? (last.querySelector('h6')?.textContent.trim() || '') : '';
                        label.textContent = `已选择 ${checked.length} 个用例（最后：${lastName}） · ${visibleCount} 个可见`;
                    }
                }
                // 更新精简进度文本并刷新向导进度 UI（替代已移除的 #testCaseCount）
                try { updateWizardProgressUI(); } catch(e) {}
                if (select) select.value = checked.length ? checked[0].value : '';
                    // 生成最后一个选中的单用例预览
                    try {
                        // Prefer the card that just triggered the change (lastCard) when it was a check action.
                        // Fallback to the last checked checkbox in DOM order if lastCard isn't available.
                        let lastCardEl = null;
                        if (lastChecked && lastCard) lastCardEl = lastCard;
                        else lastCardEl = checked.length ? checked[checked.length-1].closest('.testcase-card') : null;

                        if (lastCardEl) {
                            const cfg = JSON.parse(lastCardEl.dataset.config || '{}');
                            const nm = lastCardEl.querySelector('h6')?.textContent.trim() || '';
                            // Ensure wrapper visible
                            try { const wrapper = document.getElementById('fioCmdWrapper'); if (wrapper) wrapper.classList.remove('d-none'); } catch(e) {}
                            // If existing helper exists, prefer it
                            try {
                                if (typeof generateFioPreviewSingle === 'function') { generateFioPreviewSingle(nm, cfg); }
                                else {
                                    // Fallback: build a minimal fio command preview from cfg
                                    const fioPreview = document.getElementById('fioCmdPreview');
                                    if (fioPreview) {
                                        const parts = ['fio'];
                                        try {
                                            // map some common fio config keys
                                            if (cfg.filename) parts.push(`--filename=${cfg.filename}`);
                                            if (cfg.bs) parts.push(`--bs=${cfg.bs}`);
                                            if (cfg.iodepth) parts.push(`--iodepth=${cfg.iodepth}`);
                                            if (cfg.numjobs) parts.push(`--numjobs=${cfg.numjobs}`);
                                            if (cfg.rw) parts.push(`--rw=${cfg.rw}`);
                                        } catch(e) {}
                                        fioPreview.textContent = parts.join(' ');
                                    }
                                }
                            } catch(e) {
                                try { const fioPreview = document.getElementById('fioCmdPreview'); if (fioPreview) fioPreview.textContent = ''; } catch(_) {}
                            }
                        } else {
                            const fioPreview = document.getElementById('fioCmdPreview');
                            const wrapper = document.getElementById('fioCmdWrapper');
                            if (fioPreview) fioPreview.textContent='';
                            if (wrapper) wrapper.classList.add('d-none');
                        }
                    } catch(e) {}
                // 生成整体多用例聚合（不覆盖单用例预览，仅用于复制按钮）
                try { generateMultiFioPreview(checked.map(cb => cb.closest('.testcase-card'))); } catch(e) {}
                try { const nextBtn = document.getElementById('wizardNextBtn'); if (nextBtn) nextBtn.disabled = !checked.length; } catch(e) {}
                // rebuild execution plan when multi-select changes
                try { rebuildExecutionPlanFromSelection(); } catch(e) {}
function rebuildExecutionPlanFromSelection() {
    const checked = Array.from(document.querySelectorAll('#testCaseList .testcase-checkbox:checked'));
    if (checked.length < 1) {
        // no need to show plan for zero selections
        window.XFIO_EXEC_PLAN.steps = [];
        try { renderExecutionPlanUI(); } catch(e) {}
        return;
    }
    const plan = [];
    checked.forEach((cb, idx) => {
        const card = cb.closest('.testcase-card');
        if (!card) return;
        const tcid = cb.value;
        const cfg = JSON.parse(card.dataset.config || '{}');
        plan.push({ step: idx+1, test_case_id: tcid, name: card.querySelector('h6')?.textContent.trim() || '', cfg: cfg, run_mode: 'auto', delay_after: 60 });
    });
    window.XFIO_EXEC_PLAN.steps = plan;
    renderExecutionPlanUI();
}

function movePlanStepUp(idx) {
    if (idx > 0) {
        movePlanStep(idx, idx - 1);
        renderExecutionPlanUI();
    }
}

function movePlanStepDown(idx) {
    if (!window.XFIO_EXEC_PLAN || !Array.isArray(window.XFIO_EXEC_PLAN.steps)) return;
    const arr = window.XFIO_EXEC_PLAN.steps;
    if (idx < 0 || idx >= arr.length - 1) return; // already bottom
    movePlanStep(idx, idx + 1);
    renderExecutionPlanUI();
}

// expose reorder helpers globally for inline onclick handlers
window.movePlanStepUp = movePlanStepUp;
window.movePlanStepDown = movePlanStepDown;

function updateDelay(idx, value) {
    if (!window.XFIO_EXEC_PLAN || !Array.isArray(window.XFIO_EXEC_PLAN.steps)) return;
    const steps = window.XFIO_EXEC_PLAN.steps;
    if (!steps[idx]) return;
    let v = parseInt(value, 10);
    if (isNaN(v)) v = 0;
    if (v < 0) v = 0;
    if (v > 9999) v = 9999;
    steps[idx].delay_after = v;
}

// Core reorder helper: toIdx is the final index AFTER move (0..length-1)
function movePlanStep(fromIdx, toIdx) {
    if (!window.XFIO_EXEC_PLAN || !Array.isArray(window.XFIO_EXEC_PLAN.steps)) return;
    const arr = window.XFIO_EXEC_PLAN.steps;
    const len = arr.length;
    if (len === 0) return;
    if (fromIdx < 0 || fromIdx >= len) return;
    if (toIdx < 0) toIdx = 0;
    if (toIdx >= len) toIdx = len - 1;
    if (fromIdx === toIdx) return;
    const item = arr.splice(fromIdx,1)[0];
    arr.splice(toIdx,0,item);
    arr.forEach((s,i)=> s.step = i+1);
}

function renderExecutionPlanUI() {
    const panel = document.getElementById('executionPlanPanel');
    const list = document.getElementById('executionPlanList');
    if (!panel || !list) return;
    const steps = (window.XFIO_EXEC_PLAN && window.XFIO_EXEC_PLAN.steps) ? window.XFIO_EXEC_PLAN.steps : [];
    if (!steps.length) { panel.classList.add('d-none'); list.innerHTML = ''; return; }
    panel.classList.remove('d-none');
    try { console.debug && console.debug('renderExecutionPlanUI called, steps=', steps.length); } catch(e) {}
    list.innerHTML = '';

    // Create a vertical list for the execution plan
    const ul = document.createElement('ul');
    ul.className = 'plan-clean-list list-group';
    list.appendChild(ul);

    steps.forEach((s, idx) => {
        // Find the testcase to get name
        const testcase = window.XFIO_TESTCASES.find(tc => tc.id == s.test_case_id);
        const name = testcase ? testcase.name : s.name || s.test_case_id;
        // Extract io mode from cfg: W{numjobs}D{iodepth}
        const cfg = s.cfg || {};
        const numjobs = cfg.numjobs || cfg.num_jobs || 1;
        const iodepth = cfg.iodepth || cfg.depth || 1;
        const ioMode = `W${numjobs}D${iodepth}`;
        // Extract block size
        const bs = cfg.bs || cfg.blocksize || 'unknown';
        // Extract rw mode
        const rawRw = cfg.rw || cfg['--rw'] || cfg['rw_mode'] || null;
        const rwLabel = rawRw ? (String(rawRw).charAt(0).toUpperCase() + String(rawRw).slice(1).toLowerCase()) : 'N/A';

        const li = document.createElement('li');
        li.className = 'plan-step-item list-group-item d-flex justify-content-between align-items-center';
        li.draggable = true;
        li.dataset.idx = idx;
        const disableUp = idx === 0 ? 'disabled aria-disabled="true"' : '';
        const disableDown = idx === (steps.length - 1) ? 'disabled aria-disabled="true"' : '';
        li.innerHTML = `
            <div class="d-flex w-100 align-items-center">
                <div style="width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${name}</div>
                <div style="width: 90px;"><span class="badge-status badge">${rwLabel}</span></div>
                <div style="width: 70px;"><span class="badge bg-info text-white">${bs}</span></div>
                <div style="width: 90px;"><span class="badge bg-secondary text-white">${ioMode}</span></div>
                <div style="width: 70px; display: flex; justify-content: flex-end; gap: 0.25rem;">
                    <button type="button" class="btn btn-outline-secondary" ${disableUp} style="padding: 0.1rem 0.2rem; font-size: 0.8rem;" onclick="movePlanStepUp(${idx})" title="上移" tabindex="${idx===0?'-1':'0'}"><i class="fas fa-arrow-up"></i></button>
                    <button type="button" class="btn btn-outline-secondary" ${disableDown} style="padding: 0.1rem 0.2rem; font-size: 0.8rem;" onclick="movePlanStepDown(${idx})" title="下移" tabindex="${idx===steps.length-1?'-1':'0'}"><i class="fas fa-arrow-down"></i></button>
                </div>
                <div style="width: 120px; margin-left: auto; text-align: right; white-space: nowrap; display:flex; align-items:center; justify-content:flex-end; gap:3px;">
                    <span style="font-size:0.72rem; color:#475569;">延迟</span>
                    <input type="number" class="form-control form-control-sm" style="width: 50px; height:20px; font-size:0.8rem; line-height:1; padding:0 0.25rem; text-align:center;" aria-label="延迟秒数" value="${s.delay_after}" min="0" max="9999" step="1" onchange="updateDelay(${idx}, this.value)">
                    <span style="font-size:0.72rem; color:#475569;">秒启动</span>
                </div>
            </div>
        `;
        ul.appendChild(li);
    });

    // Bind drag-and-drop
    enablePlanDragAndDrop(list);
}

function enablePlanDragAndDrop(list) {
    if (!list) return;
    // Remove existing bindings if any
    if (list.__dragBound) return; // already bound
    list.__dragBound = true;
    try { console.debug && console.debug('enablePlanDragAndDrop bound'); } catch(e) {}

    let dragSrcIdx = null;

    list.addEventListener('dragstart', function(e){
        const el = e.target.closest && e.target.closest('.plan-step-item');
        if (!el) return;
        dragSrcIdx = parseInt(el.dataset.idx);
        e.dataTransfer.effectAllowed = 'move';
        try { e.dataTransfer.setData('text/plain', String(dragSrcIdx)); } catch(_){}
        el.classList.add('dragging');
    });

    list.addEventListener('dragend', function(e){
        const els = list.querySelectorAll('.plan-step-item.dragging'); els.forEach(x=>x.classList.remove('dragging'));
        // cleanup any insertion markers
        const ins = list.querySelectorAll('.insert-marker'); ins.forEach(x=>x.remove());
        try { drawPlanConnectors(list); } catch(e) {}
    });

    list.addEventListener('dragover', function(e){
        e.preventDefault();
        const over = e.target.closest && e.target.closest('.plan-step-item');
        if (!over) return;
        const overIdx = parseInt(over.dataset.idx);
        // show insertion marker before/after based on mouse position
        const rect = over.getBoundingClientRect();
        const before = (e.clientX - rect.left) < (rect.width/2);
        // remove old markers
        const old = list.querySelectorAll('.insert-marker'); old.forEach(x=>x.remove());
        const marker = document.createElement('div'); marker.className = 'insert-marker';
        marker.style.width = '4px'; marker.style.background = '#1d4ed8'; marker.style.position = 'absolute';
        const parentRow = over.parentNode;
        // position marker at edge of over element
        const mRect = over.getBoundingClientRect();
        const listRect = list.getBoundingClientRect();
        marker.style.height = (mRect.height-8) + 'px';
        marker.style.top = (mRect.top - listRect.top + 4) + 'px';
        if (before) {
            marker.style.left = (mRect.left - listRect.left - 6) + 'px';
        } else {
            marker.style.left = (mRect.right - listRect.left + 2) + 'px';
        }
        list.appendChild(marker);
    });

    list.addEventListener('drop', function(e){
        e.preventDefault();
        const data = (function(){ try { return e.dataTransfer.getData('text/plain'); } catch(e) { return null; } })();
        const src = data !== null && data !== '' ? parseInt(data) : dragSrcIdx;
        const over = e.target.closest && e.target.closest('.plan-step-item');
        if (!over) return;
        const overIdx = parseInt(over.dataset.idx);
        const rect = over.getBoundingClientRect();
        const before = (e.clientX - rect.left) < (rect.width/2);
        const dest = before ? overIdx : overIdx + 1;
        if (src !== null && !isNaN(src)) {
            movePlanStep(src, dest > src ? dest-1 : dest);
        }
    });
}

function drawPlanConnectors(list) {
    // remove existing svg
    const existing = list.querySelector('.plan-connectors'); if (existing) existing.remove();
    try { console.debug && console.debug('drawPlanConnectors'); } catch(e) {}
    const grid = list.querySelector('.plan-grid'); if (!grid) return;
    // create svg overlay
    const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
    svg.setAttribute('class','plan-connectors');
    svg.setAttribute('width','100%'); svg.setAttribute('height','100%');
    svg.style.position = 'absolute'; svg.style.left = '0'; svg.style.top = '0'; svg.style.pointerEvents = 'none';
    // marker definition
    const defs = document.createElementNS('http://www.w3.org/2000/svg','defs');
    const marker = document.createElementNS('http://www.w3.org/2000/svg','marker');
    marker.setAttribute('id','arrowhead'); marker.setAttribute('markerWidth','8'); marker.setAttribute('markerHeight','8'); marker.setAttribute('refX','6'); marker.setAttribute('refY','3'); marker.setAttribute('orient','auto');
    const path = document.createElementNS('http://www.w3.org/2000/svg','path'); path.setAttribute('d','M0,0 L6,3 L0,6 Z'); path.setAttribute('fill','#1d4ed8');
    marker.appendChild(path); defs.appendChild(marker); svg.appendChild(defs);

    // append svg to list (positioned relative in CSS)
    list.appendChild(svg);

    // compute positions of each step card center
    const cards = Array.from(list.querySelectorAll('.plan-step-card'));
    if (cards.length < 2) return;
    const listRect = list.getBoundingClientRect();
    cards.forEach((c, idx) => {
        const next = cards[idx+1];
        if (!next) return;
        const r1 = c.getBoundingClientRect();
        const r2 = next.getBoundingClientRect();
        // start at right-middle of c, end at left-middle of next (screen coords)
        const startX = r1.right - listRect.left;
        const startY = (r1.top + r1.bottom)/2 - listRect.top;
        const endX = r2.left - listRect.left;
        const endY = (r2.top + r2.bottom)/2 - listRect.top;
        // create a polyline with a right-angle elbow: start -> (endX, startY) -> end
        const poly = document.createElementNS('http://www.w3.org/2000/svg','polyline');
        poly.setAttribute('points', `${startX},${startY} ${endX},${startY} ${endX},${endY}`);
        poly.setAttribute('fill','none'); poly.setAttribute('stroke','#1d4ed8'); poly.setAttribute('stroke-width','2'); poly.setAttribute('marker-end','url(#arrowhead)');
        svg.appendChild(poly);
    });
    // adjust svg height to content
    svg.setAttribute('height', list.scrollHeight + 'px');
}

function enablePlanDragAndDrop(list) {
    if (!list) return;
    // Remove existing bindings if any
    if (list.__dragBound) return; // already bound
    list.__dragBound = true;
    try { console.debug && console.debug('enablePlanDragAndDrop bound'); } catch(e) {}

    let dragSrcIdx = null;

    list.addEventListener('dragstart', function(e){
        const el = e.target.closest && e.target.closest('.plan-step-item');
        if (!el) return;
        dragSrcIdx = parseInt(el.dataset.idx);
        e.dataTransfer.effectAllowed = 'move';
        try { e.dataTransfer.setData('text/plain', String(dragSrcIdx)); } catch(_){}
        el.classList.add('dragging');
    });

    list.addEventListener('dragend', function(e){
        const els = list.querySelectorAll('.plan-step-item.dragging'); els.forEach(x=>x.classList.remove('dragging'));
        // cleanup any insertion markers
        const ins = list.querySelectorAll('.insert-marker'); ins.forEach(x=>x.remove());
    });

    list.addEventListener('dragover', function(e){
        e.preventDefault();
        const over = e.target.closest && e.target.closest('.plan-step-item');
        if (!over) return;
        const overIdx = parseInt(over.dataset.idx);
        // show insertion marker before/after based on mouse position
        const rect = over.getBoundingClientRect();
        const before = (e.clientX - rect.left) < (rect.width/2);
        // remove old markers
        const old = list.querySelectorAll('.insert-marker'); old.forEach(x=>x.remove());
        const marker = document.createElement('div');
        marker.className = 'insert-marker';
        marker.style.height = '2px';
        marker.style.backgroundColor = '#007bff';
        marker.style.position = 'absolute';
        marker.style.left = '0';
        marker.style.right = '0';
        marker.style.zIndex = '10';
        if (before) {
            marker.style.top = rect.top + 'px';
            over.parentNode.insertBefore(marker, over);
        } else {
            marker.style.top = (rect.bottom - 2) + 'px';
            over.parentNode.insertBefore(marker, over.nextSibling);
        }
    });

    list.addEventListener('drop', function(e){
        e.preventDefault();
        const over = e.target.closest && e.target.closest('.plan-step-item');
        if (!over || dragSrcIdx === null) return;
        const overIdx = parseInt(over.dataset.idx);
        const rect = over.getBoundingClientRect();
        const before = (e.clientX - rect.left) < (rect.width/2);
        const toIdx = before ? overIdx : overIdx + 1;
        movePlanStep(dragSrcIdx, toIdx);
        dragSrcIdx = null;
    });
}

// (removed duplicate movePlanStep definition)
            }

            // 应用过滤器的方法：基于当前 testCaseFilters 隐/显卡片
            function applyTestCaseFilters() {
                // 简单子串匹配：输入整体作为一个关键字，包含则显示，不包含则隐藏
                const rawQ = (testCaseFilters.q || '').trim().toLowerCase();
                const cards = list ? Array.from(list.children) : [];
                let visible = 0;
                cards.forEach(card => {
                    // 解析卡上的 fio config（存在于 dataset.config）
                    let cfg = {};
                    try { cfg = JSON.parse(card.dataset.config || '{}'); } catch (e) { cfg = {}; }

                    // extract values with fallbacks
                    const rwVal = (cfg.rw || cfg['--rw'] || cfg.rw_mode || '').toString().toLowerCase();
                    const bsVal = (cfg.bs || cfg.block_size || '').toString().toLowerCase();
                    const iodepthVal = (cfg.iodepth !== undefined && cfg.iodepth !== null) ? String(cfg.iodepth) : '';
                    const numjobsVal = (cfg.numjobs !== undefined && cfg.numjobs !== null) ? String(cfg.numjobs) : '';

                    // text search (use cached dataset.search to avoid layout thrashing)
                    const txt = (card.dataset && card.dataset.search) ? card.dataset.search : ((card.innerText || '').toLowerCase());
                    if (rawQ && txt.indexOf(rawQ) === -1) { card.style.display = 'none'; return; }

                    // rw filter
                    if (testCaseFilters.rw !== 'all' && (!rwVal || rwVal.indexOf(testCaseFilters.rw) === -1)) {
                        card.style.display = 'none';
                        return;
                    }

                    // bs filter (match prefix like '128k' or '4k')
                    if (testCaseFilters.bs !== 'all') {
                        if (!bsVal || bsVal.indexOf(testCaseFilters.bs) === -1) {
                            card.style.display = 'none';
                            return;
                        }
                    }

                    // iodepth filter
                    if (testCaseFilters.iodepth !== 'all') {
                        const want = String(testCaseFilters.iodepth);
                        const have = iodepthVal || '1';
                        if (have !== want) {
                            card.style.display = 'none';
                            return;
                        }
                    }

                    // numjobs filter
                    if (testCaseFilters.numjobs !== 'all') {
                        const want = String(testCaseFilters.numjobs);
                        const have = numjobsVal || '1';
                        if (have !== want) {
                            card.style.display = 'none';
                            return;
                        }
                    }

                    // 通过所有过滤器
                    card.style.display = '';
                    visible++;
                });
                if (countBadge) countBadge.textContent = visible;
                // 更新无障碍播报
                try { const aria = document.getElementById('ariaStatus'); if (aria) aria.textContent = `${visible} 个用例可见`; } catch (e) {}
            }

            // 搜索输入已移除（服务器端/页面上无该控件）

            // 绑定并填充下拉筛选（动态从 loaded test cases 中提取唯一的值）
            function bindFilterSelects() {
                const selRw = document.getElementById('filterRw');
                const selBs = document.getElementById('filterBs');
                const selIodepth = document.getElementById('filterIodepth');
                const selNumjobs = document.getElementById('filterNumjobs');

                // collect unique values from XFIO_TESTCASES
                const rws = new Set();
                const bss = new Set();
                const iodeps = new Set();
                const njs = new Set();
                XFIO_TESTCASES.forEach(tc => {
                    const cfg = tc.fio_config || {};
                    const rwv = (cfg.rw || cfg['--rw'] || cfg.rw_mode || '') + '';
                    const bsv = (cfg.bs || cfg.block_size || '') + '';
                    const iodepv = (cfg.iodepth !== undefined && cfg.iodepth !== null) ? String(cfg.iodepth) : '';
                    const njv = (cfg.numjobs !== undefined && cfg.numjobs !== null) ? String(cfg.numjobs) : '';
                    if (rwv) rws.add(String(rwv).toLowerCase());
                    if (bsv) bss.add(String(bsv).toLowerCase());
                    if (iodepv) iodeps.add(iodepv);
                    if (njv) njs.add(njv);
                });

                // helper to populate a select
                function populate(sel, items, transform) {
                    if (!sel) return;
                    // clear except the first option
                    while (sel.options.length > 1) sel.remove(1);
                    Array.from(items).sort().forEach(it => {
                        const opt = document.createElement('option');
                        opt.value = transform ? transform(it) : it;
                        opt.textContent = (transform ? transform(it) : it).toString().toUpperCase();
                        sel.appendChild(opt);
                    });
                }

                populate(selRw, rws, v => v);
                populate(selBs, bss, v => v);
                populate(selIodepth, Array.from(iodeps).sort((a,b)=>Number(a)-Number(b)), v => v);
                populate(selNumjobs, Array.from(njs).sort((a,b)=>Number(a)-Number(b)), v => v);

                // change handlers
                if (selRw) selRw.addEventListener('change', function() { testCaseFilters.rw = this.value || 'all'; applyTestCaseFilters(); });
                if (selBs) selBs.addEventListener('change', function() { testCaseFilters.bs = this.value || 'all'; applyTestCaseFilters(); });
                if (selIodepth) selIodepth.addEventListener('change', function() { testCaseFilters.iodepth = this.value || 'all'; applyTestCaseFilters(); });
                if (selNumjobs) selNumjobs.addEventListener('change', function() { testCaseFilters.numjobs = this.value || 'all'; applyTestCaseFilters(); });

                const clearBtn = document.getElementById('clearTestCaseFilters');
                if (clearBtn) {
                    clearBtn.addEventListener('click', function() {
                        testCaseFilters.rw = 'all'; testCaseFilters.bs = 'all'; testCaseFilters.iodepth = 'all'; testCaseFilters.numjobs = 'all'; testCaseFilters.q = '';
                        try {
                            const sci = document.getElementById('testCaseSearch');
                            if (sci) sci.value = '';
                        } catch(e) {}
                        try { if (selRw) selRw.value = 'all'; if (selBs) selBs.value = 'all'; if (selIodepth) selIodepth.value = 'all'; if (selNumjobs) selNumjobs.value = 'all'; } catch(e) {}
                        applyTestCaseFilters();
                    });
                }
            }

            // 初始化绑定一次
            bindFilterSelects();
            // 首次应用（默认展示全部）
            applyTestCaseFilters();
            // End timing for rendering test cases
            try { console.timeEnd && console.timeEnd('renderTestCases'); } catch (e) {}
            try {
                var __xfio_render_end = (window.performance && performance.now) ? performance.now() : Date.now();
                var __xfio_render_ms = Math.max(0, (__xfio_render_end - (__xfio_render_start || __xfio_render_end)));
                try {
                    fetch('/api/debug/client-timing', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            event: 'renderTestCases',
                            count: XFIO_TESTCASES.length,
                            duration_ms: Math.round(__xfio_render_ms),
                            ts: new Date().toISOString(),
                            userAgent: navigator && navigator.userAgent ? navigator.userAgent : 'unknown'
                        })
                    }).catch(function(){ });
                } catch(e) {}
            } catch(e) {}
        })
        .catch(error => {
            console.error('Error loading test cases:', error);
            const select = document.getElementById('testCaseSelect');
            select.innerHTML = '<option value="">加载测试用例失败</option>';
        });
}

// 全局缓存 workers，用于在添加配置覆盖时读取磁盘信息
let XFIO_WORKERS = [];
// 每个 worker 的配置缓存，避免在只显示一个卡片模式下切换丢失输入
window.__xfio_worker_config_cache = window.__xfio_worker_config_cache || {};
// 统一与单独覆盖状态容器
window.XFIO_ASSIGN_STATE = window.XFIO_ASSIGN_STATE || {
    global: { filename: '' },
    overrides: {}, // workerId -> { filename }
};

function renderWorkerOverridesUI() {
    const state = window.XFIO_ASSIGN_STATE;
    const listEl = document.getElementById('workerOverridesList');
    const wrap = document.getElementById('workerOverridesWrapper');
    const overrideCountEl = document.getElementById('overrideCount');
    // buttons removed from template: applyGlobalToEmptyBtn, overrideAllWithGlobalBtn
    if (!listEl) return;
    const entries = Object.entries(state.overrides);
    if (overrideCountEl) overrideCountEl.textContent = entries.length;
    if (wrap) wrap.classList.toggle('d-none', entries.length === 0);
    // overrideAllBtn removed; nothing to toggle
    // render rows
    listEl.innerHTML = '';
    entries.forEach(([wid, cfg]) => {
        const worker = (XFIO_WORKERS || []).find(w => w.id === wid) || { name: wid };
        const row = document.createElement('div');
        row.className = 'override-row';
        row.dataset.workerId = wid;
        row.innerHTML = `
            <div class="ov-worker" title="${worker.name}">${worker.name}</div>
            <input type="text" class="form-control form-control-sm ov-path" value="${cfg.filename || ''}" placeholder="继承: ${state.global.filename || '(未设置全局)'}" aria-label="${worker.name} 覆盖路径">`;
        listEl.appendChild(row);
    });
    // attach events
    listEl.querySelectorAll('.ov-path').forEach(inp => {
        if (inp.__bound) return; inp.__bound = true;
        inp.addEventListener('input', debounce(function(){
            const row = this.closest('.override-row');
            if (!row) return;
            const wid = row.dataset.workerId;
            if (!wid) return;
            if (!window.XFIO_ASSIGN_STATE.overrides[wid]) window.XFIO_ASSIGN_STATE.overrides[wid] = {};
            window.XFIO_ASSIGN_STATE.overrides[wid].filename = this.value.trim();

            // 实时同步对应 worker 卡片的路径提示与 has-override 状态（无需重新渲染 overrides 列表，保留输入焦点）
            try {
                const cardCb = document.getElementById(`worker-${wid}`);
                const card = cardCb ? cardCb.closest('.worker-card') : null;
                const ov = window.XFIO_ASSIGN_STATE.overrides[wid] ? (window.XFIO_ASSIGN_STATE.overrides[wid].filename || '').trim() : '';
                const g = (window.XFIO_ASSIGN_STATE.global && window.XFIO_ASSIGN_STATE.global.filename) ? window.XFIO_ASSIGN_STATE.global.filename.trim() : '';
                const effective = ov || g || '';
                if (card) {
                    if (ov) card.classList.add('has-override'); else card.classList.remove('has-override');
                    const pathWrap = card.querySelector('.wk-path-hint');
                    const pathHint = pathWrap ? pathWrap.querySelector('em') : null;
                    if (effective) {
                        if (pathWrap) pathWrap.classList.remove('d-none');
                        if (pathHint) pathHint.textContent = effective;
                        if (pathWrap) pathWrap.setAttribute('title', `路径: ${effective}`);
                    } else {
                        if (pathWrap) pathWrap.classList.add('d-none');
                        if (pathHint) pathHint.textContent = '';
                        if (pathWrap) pathWrap.removeAttribute('title');
                    }
                }
            } catch(e) {}

            // 更新计数与校验状态
            try { const oc = document.getElementById('overrideCount'); if (oc) oc.textContent = Object.keys(window.XFIO_ASSIGN_STATE.overrides || {}).length; } catch(e) {}
            updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {}
        }, 250));
        // Enter 键等同于点击该行的应用按钮
        inp.addEventListener('keydown', function(e){
            if (e.key === 'Enter') {
                e.preventDefault();
                const row = this.closest('.override-row'); if (!row) return;
                const applyBtn = row.querySelector('.apply-override');
                if (applyBtn) applyBtn.click();
            }
        });
    });
    // apply/restore buttons removed: input now updates state in real-time
    // worker 卡片标记与路径提示同步：显示有效路径（优先使用覆盖，其次使用全局）
    try {
        const globalPath = (state.global && state.global.filename) ? state.global.filename.trim() : '';
        document.querySelectorAll('#workerSelectionArea .worker-card').forEach(card => {
            const cb = card.querySelector('.worker-checkbox') || {};
            const wid = cb.value; if (!wid) return;
            const ov = state.overrides && state.overrides[wid] ? (state.overrides[wid].filename || '').trim() : '';
            const effective = ov || globalPath || '';
            // has-override 标记仅在存在显式覆盖时显示
            if (ov) card.classList.add('has-override'); else card.classList.remove('has-override');
            // 更新路径提示
            try {
                const pathWrap = card.querySelector('.wk-path-hint');
                const pathHint = pathWrap ? pathWrap.querySelector('em') : null;
                if (effective) {
                    if (pathWrap) pathWrap.classList.remove('d-none');
                    if (pathHint) pathHint.textContent = effective;
                    if (pathWrap && effective) pathWrap.setAttribute('title', `路径: ${effective}`);
                } else {
                    if (pathWrap) pathWrap.classList.add('d-none');
                    if (pathHint) pathHint.textContent = '';
                    if (pathWrap) pathWrap.removeAttribute('title');
                }
            } catch(e) {}
        });
    } catch(e) {}
    // applyGlobalToEmpty and overrideAllWithGlobal handlers removed
    const collapseBtn = document.getElementById('collapseOverridesBtn');
    if (collapseBtn && !collapseBtn.__bound) {
        collapseBtn.__bound = true;
        collapseBtn.addEventListener('click', function(){
            const wrapEl = document.getElementById('workerOverridesWrapper');
            if (!wrapEl) return; wrapEl.classList.toggle('collapsed');
            this.textContent = wrapEl.classList.contains('collapsed') ? '展开' : '折叠';
        });
    }
    // 已移除：清除全部覆盖的按钮及其处理逻辑
}

function ensureOverride(workerId) {
    const st = window.XFIO_ASSIGN_STATE;
    if (!st.overrides[workerId]) st.overrides[workerId] = { filename: st.global.filename || '' };
    renderWorkerOverridesUI();
}

function debounce(fn, wait) { let t; return function(){ const ctx=this, args=arguments; clearTimeout(t); t=setTimeout(()=>fn.apply(ctx,args), wait); }; }

// 生成单个测试用例的 fio 多行预览，补齐常用重要参数并渲染到 #fioCmdPreview
function generateFioPreviewSingle(name, cfg) {
    // 设定一些合理默认值
    const defaults = {
        filename: cfg.filename || '/dev/sdX',
        bs: cfg.bs || cfg.block_size || '4k',
        iodepth: cfg.iodepth !== undefined ? cfg.iodepth : 1,
        numjobs: cfg.numjobs !== undefined ? cfg.numjobs : 1,
        rw: cfg.rw || cfg['--rw'] || cfg.rw_mode || 'read',
        // 默认强制 time-based 运行，默认 180s（如果cfg指定则使用其值）
        runtime: (cfg.runtime !== undefined && cfg.runtime !== null) ? cfg.runtime : 180,
        ioengine: cfg.ioengine || 'libaio'
    };

    // Build multi-line fio .ini style for readability
    const lines = [];
    lines.push(`# ${name}`);
    lines.push('[global]');
    lines.push(`ioengine=${defaults.ioengine}`);
    lines.push(`runtime=${defaults.runtime}`);
    lines.push(`bs=${defaults.bs}`);
    lines.push(`iodepth=${defaults.iodepth}`);
    lines.push(`numjobs=${defaults.numjobs}`);
    lines.push(`rw=${defaults.rw}`);
    // add extra options from cfg if present (preserve keys)
    // 允许直接透传的一些可选项，但注意不要传入会导致按写入量终止的选项
    const passthrough = ['invalidate','group_reporting'];
    passthrough.forEach(k => { if (cfg[k] !== undefined) {
        const v = cfg[k];
        // normalize boolean to 1/0
        if (typeof v === 'boolean') lines.push(`${k}=${v ? '1' : '0'}`);
        else lines.push(`${k}=${v}`);
    }});

    // direct: 如果用户未显式指定则默认启用 direct=1；如果指定则保留其值（并规范为 1/0）
    if (cfg.direct !== undefined) {
        const dv = cfg.direct;
        if (typeof dv === 'boolean') lines.push(`direct=${dv ? '1' : '0'}`);
        else lines.push(`direct=${dv}`);
    } else {
        lines.push('direct=1');
    }

    // 如果用户在 cfg 中填写了 size/filesize 相关的选项，我们刻意忽略它们以保证 time-based
    const sizeKeys = ['size','filesize','size_mb','filesize_mb','file_size','blocksize_bytes'];
    const foundSizeKeys = sizeKeys.filter(k => cfg[k] !== undefined);
    if (foundSizeKeys.length) {
        lines.push(`# 注意：已省略 ${foundSizeKeys.join(', ')} 等按写入量终止的选项，任务将使用 time_based=1 运行`);
    }
    // 强制 time-based
    lines.push('time_based=1');
    lines.push('');
    lines.push('[job]');
    lines.push(`filename=${defaults.filename}`);
    // render to preview element
    try {
        const fioPreview = document.getElementById('fioCmdPreview');
        const wrapper = document.getElementById('fioCmdWrapper');
        if (fioPreview) fioPreview.textContent = lines.join('\n');
        if (wrapper) wrapper.classList.remove('d-none');
    } catch(e) {}
}


// 加载Worker用于任务分配
function loadWorkersForAssignment() {
    fetch('/api/workers')
        .then(response => response.json())
        .then(workers => {
            XFIO_WORKERS = Array.isArray(workers) ? workers : [];
            const container = document.getElementById('workerSelectionArea');
            container.innerHTML = '';

            if (!XFIO_WORKERS || XFIO_WORKERS.length === 0) {
                container.innerHTML = '<div class="card"><div class="card-body"><p class="text-muted mb-0">暂无可用的Worker节点</p></div></div>';
                return;
            }

            // 统一为与测试用例相同风格的网格卡片
            try { container.classList.add('worker-grid'); } catch(e) {}
            XFIO_WORKERS.forEach(worker => {
                const card = document.createElement('div');
                card.className = 'worker-card';
                card.setAttribute('tabindex', '0');
                card.setAttribute('role', 'listitem');
                const ip = worker.ip_address || worker.ip || '';
                const status = (worker.status || '').toLowerCase();
                const hb = (worker.heartbeat_status || '').toLowerCase();
                function statusColor() {
                    if (status === 'online' || hb === 'healthy') return '#10b981';
                    if (status === 'busy') return '#f59e0b';
                    if (status === 'offline' || hb === 'dead') return '#ef4444';
                    return '#6b7280';
                }
                const dotColor = statusColor();
                const statusText = worker.status || 'unknown';
                const globalPathVal = (window.XFIO_ASSIGN_STATE.global.filename||'').trim();
                // 提取 CPU / 内存信息
                const cpuVal = worker.cpu_cores || worker.cores || worker.cpu || worker.num_cpus || worker.n_cpus || worker.core_count;
                let memBytes = worker.memory_bytes || worker.mem_bytes || worker.total_memory_bytes || worker.mem_total_bytes;
                let memGb = worker.memory_gb || worker.mem_gb || worker.total_memory_gb;
                if (!memGb && memBytes) {
                    memGb = memBytes / (1024*1024*1024);
                }
                let memStr = '';
                if (memGb) {
                    if (memGb >= 1) {
                        const rounded = Math.abs(memGb - Math.round(memGb)) < 0.15 ? Math.round(memGb) : Math.round(memGb*10)/10;
                        memStr = `${rounded}G`;
                    } else {
                        // 小于 1G 显示 M
                        const mb = (memGb * 1024).toFixed(0);
                        memStr = `${mb}M`;
                    }
                }
                const cpuStr = cpuVal ? `${cpuVal}C` : '';
                const resStr = (cpuStr || memStr) ? `${cpuStr}${memStr}` : '';
                card.innerHTML = `
                    <input type="checkbox" class="worker-checkbox" id="worker-${worker.id}" value="${worker.id}" aria-label="选择 ${worker.name}" hidden>
                    <div class="wk-head">
                        <h6 class="wk-name" title="${worker.name}">${worker.name}</h6>
                    </div>
                    <div class="wk-sub">
                        ${ip ? `<span class=\"wk-ip\" title=\"IP: ${ip}\">${ip}</span>` : '<span class="wk-ip text-muted" title="无IP">--</span>'}
                        ${resStr ? `<span class=\"wk-res\" title=\"CPU/内存\">${resStr}</span>` : ''}
                    </div>
                    <div class="wk-foot">
                        <span class="wk-status badge-status" data-status="${statusText.toLowerCase()}" title="状态: ${statusText}">${statusText}</span>
                        <span class=\"wk-path-hint d-none\" data-worker-id=\"${worker.id}\" title=\"节点专属覆盖路径\"><em></em></span>
                    </div>
                    <button type="button" class="set-override" title="编辑该节点路径覆盖" aria-label="为 ${worker.name} 设置/编辑路径">编辑路径</button>`;
                const mark = document.createElement('div');
                mark.className = 'select-mark';
                mark.innerHTML = '<i class="fas fa-check"></i>';
                card.appendChild(mark);
                container.appendChild(card);

                // 点击切换选中 / 取消
                card.addEventListener('click', function(e) {
                    const ignorable = ['A','BUTTON','INPUT','SELECT','TEXTAREA','LABEL'];
                    if (e.target && e.target.tagName && ignorable.includes(e.target.tagName)) return;
                    const cb = card.querySelector('.worker-checkbox');
                    if (!cb) return;
                    cb.checked = !cb.checked;
                    cb.dispatchEvent(new Event('change', { bubbles: true }));
                });
                card.addEventListener('keydown', function(e) {
                    if (e.key === ' ' || e.key === 'Spacebar' || e.key === 'Enter') {
                        e.preventDefault();
                        const cb = card.querySelector('.worker-checkbox');
                        if (!cb) return;
                        cb.checked = !cb.checked;
                        cb.dispatchEvent(new Event('change', { bubbles: true }));
                    }
                });
            });

            // 绑定复选框 change 以同步卡片样式
            document.querySelectorAll('#workerSelectionArea .worker-card .worker-checkbox').forEach(cb => {
                cb.addEventListener('change', function(){
                    const card = this.closest('.worker-card');
                    if (card) card.classList.toggle('selected', this.checked);
                    renderWorkerOverridesUI();
                });
            });

            // 绑定“路径”按钮点击：若未选中则先选中，再创建/聚焦覆盖输入
            document.querySelectorAll('#workerSelectionArea .worker-card .set-override').forEach(btn => {
                btn.addEventListener('click', function(e){
                    e.stopPropagation();
                    const card = this.closest('.worker-card');
                    if (!card) return;
                    const cb = card.querySelector('.worker-checkbox');
                    if (cb && !cb.checked) { cb.checked = true; cb.dispatchEvent(new Event('change', { bubbles: true })); }
                    const wid = cb ? cb.value : null;
                    if (!wid) return;
                    const st = window.XFIO_ASSIGN_STATE;
                    if (!st.overrides[wid]) {
                        st.overrides[wid] = { filename: st.global.filename || '' };
                        renderWorkerOverridesUI();
                    }
                    // 聚焦对应输入框并闪烁提示
                    setTimeout(() => {
                        const row = document.querySelector(`#workerOverridesList .override-row[data-worker-id="${wid}"]`);
                        if (row) {
                            const inp = row.querySelector('.ov-path');
                            row.classList.add('flash');
                            setTimeout(()=>row.classList.remove('flash'), 1200);
                            if (inp) { inp.focus(); inp.select(); }
                        }
                        // 确保覆盖区域可见
                        const wrap = document.getElementById('workerOverridesWrapper');
                        if (wrap && wrap.classList.contains('d-none')) wrap.classList.remove('d-none');
                        const overrideAnchor = document.getElementById('workerOverridesWrapper');
                        if (overrideAnchor) {
                            try { overrideAnchor.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch(_) {}
                        }
                        // 同步该 worker 卡片路径提示（如果已经有覆盖）
                        try {
                            const pathWrap = card.querySelector('.wk-path-hint');
                            const pathHint = pathWrap ? pathWrap.querySelector('em') : null;
                            const ov = st.overrides[wid];
                            if (pathWrap && pathHint && ov && ov.filename) { pathHint.textContent = ov.filename; pathWrap.classList.remove('d-none'); }
                        } catch(e) {}
                    }, 30);
                });
            });

            // 绑定 worker 搜索与批量操作按钮（在模态中）
            const workerSearch = document.getElementById('workerSearch');

            if (workerSearch) {
                let wt = null;
                workerSearch.addEventListener('input', function() {
                    if (wt) clearTimeout(wt);
                    wt = setTimeout(() => {
                        const q = workerSearch.value.trim().toLowerCase();
                        document.querySelectorAll('#workerSelectionArea .worker-card').forEach(card => {
                            const text = card.innerText.toLowerCase();
                            const ok = !q || text.indexOf(q) !== -1;
                            card.style.display = ok ? '' : 'none';
                        });
                        // 更新可见计数
                        updateVisibleSelectedBadges();
                    }, 160);
                });
            }

            // 批量选择功能已移除，仅保留搜索


            // 更新可见/已选 徽章
            // 保留一个简化的无障碍更新，仅更新可见/已选文本（无徽章）
            function updateVisibleSelectedBadges() {
                try {
                    const visibleCards = Array.from(document.querySelectorAll('#workerSelectionArea .worker-card')).filter(c => c.style.display !== 'none');
                    const selectedCount = Array.from(document.querySelectorAll('#workerSelectionArea .worker-card .worker-checkbox')).filter(cb => cb.checked).length;
                    const aria = document.getElementById('ariaStatus');
                    if (aria) aria.textContent = `${visibleCards.length} 个可见，${selectedCount} 个已选`;
                } catch(e) {}
            }
            updateVisibleSelectedBadges();
        })
        .catch(error => {
            console.error('Error loading workers:', error);
            const container = document.getElementById('workerSelectionArea');
            if (container) {
                container.innerHTML = '<div class="card"><div class="card-body"><p class="text-danger mb-0">加载Worker节点失败: ' + (error && error.message ? error.message : error) + '</p></div></div>';
            }
        });

    // 全局路径输入绑定
    const globalInput = document.getElementById('globalTestPathInput');
    if (globalInput && !globalInput.__bound) {
        globalInput.__bound = true;
        globalInput.addEventListener('input', debounce(function(){
            window.XFIO_ASSIGN_STATE.global.filename = this.value.trim();
            // 更新 override placeholder 文案
            renderWorkerOverridesUI();
        }, 200));
    }

    // 全局应用按钮：将输入应用为全局路径，或清空以移除全局设置
    const applyGlobalBtn = document.getElementById('applyGlobalTestPathBtn');
    if (applyGlobalBtn && !applyGlobalBtn.__bound) {
        applyGlobalBtn.__bound = true;
        applyGlobalBtn.addEventListener('click', function(){
            const gi = document.getElementById('globalTestPathInput'); if (!gi) return;
            const v = (gi.value || '').trim();
            if (v) window.XFIO_ASSIGN_STATE.global.filename = v; else window.XFIO_ASSIGN_STATE.global.filename = '';
            // re-render overrides and validation
            renderWorkerOverridesUI();
            updateWizardNextButtonState(); try { updateWizardProgressUI(); } catch(e) {}
        });
    }

    // 监听 worker 卡片中的“编辑路径”按钮出现逻辑（基于选中状态）
    try {
        document.addEventListener('click', function(e){
            if (e.target && e.target.classList && e.target.classList.contains('set-override')) {
                const cb = e.target.closest('.wk-row')?.querySelector('.worker-checkbox');
                if (cb && cb.value) {
                    if (!cb.checked) { cb.checked = true; cb.dispatchEvent(new Event('change', { bubbles:true })); }
                    ensureOverride(cb.value);
                }
            }
        });
    } catch(e) {}

    // 选中按钮可见性已在 change 事件中处理，无需额外监听
}
function addTaskConfigOverrideArea(workerId) {
    // 检查是否已存在该Worker的配置区域
    const existingArea = document.querySelector(`#configOverride-${workerId}`);
    if (existingArea) {
        existingArea.style.display = 'block';
        try { collapseOtherWorkerConfigs(workerId); } catch(e) {}
        return;
    }
    
    const overridesContainer = document.getElementById('taskConfigOverrides');
    // 若首次插入且还没有工具栏，添加一个工具栏占位（目前仅说明单卡模式，可扩展）
    if (overridesContainer && !overridesContainer.querySelector('.config-override-toolbar')) {
        const toolbar = document.createElement('div');
        toolbar.className = 'config-override-toolbar d-flex justify-content-between align-items-center mb-2';
        toolbar.innerHTML = '<small class="text-muted">当前仅显示一个节点覆盖；重新勾选其它节点可切换。</small>';
        overridesContainer.prepend(toolbar);
    }
    const worker = XFIO_WORKERS.find(w => w.id === workerId) || {};

    const configArea = document.createElement('div');
    configArea.className = 'card mb-2 config-override-card task-config-card simple-only-path';
    configArea.id = `configOverride-${workerId}`;
    configArea.setAttribute('data-worker-id', workerId);
    // 简化：仅暴露一个测试路径输入
    configArea.innerHTML = `
        <div class="card-body py-2 px-3 d-flex justify-content-between align-items-center gap-3">
            <div class="flex-grow-1">
                <label class="form-label mb-1 small text-muted">${worker.name || workerId} 测试路径</label>
                <input class="form-control form-control-sm config-file-input" id="configOverride-${workerId}-filename" placeholder="/dev/sdb 或 /mnt/data/file.img" aria-label="${worker.name || workerId} 测试路径">
                <div class="form-text mt-1">只可修改测试文件/设备路径</div>
            </div>
            <div class="d-flex flex-column align-items-end gap-1">
                <button type="button" class="btn btn-sm btn-outline-danger remove-config-override" data-worker-id="${workerId}" aria-label="移除 ${worker.name || workerId}">移除</button>
            </div>
        </div>`;
    overridesContainer.appendChild(configArea);
    // 恢复缓存的输入
    try {
        const cache = window.__xfio_worker_config_cache[workerId];
        if (cache) {
            const fi = configArea.querySelector('.config-file-input');
            if (fi && cache.filename !== undefined) fi.value = cache.filename;
        }
    } catch(e) {}
    try { collapseOtherWorkerConfigs(workerId); } catch(e) {}
    
            // 绑定移除按钮事件 (legacy path cards removed)
    configArea.querySelector('.remove-config-override').addEventListener('click', function() {
        const workerId = this.getAttribute('data-worker-id');
        persistWorkerConfig(workerId);
        removeTaskConfigOverrideArea(workerId);
        // 同时取消该Worker的选中状态（如果存在）
        const cb = document.getElementById(`worker-${workerId}`);
        if (cb) { cb.checked = false; cb.dispatchEvent(new Event('change', { bubbles: true })); }
    });

    // 高级切换按钮已移除（仅保留路径）

    // 当该卡被加入页面后，监听 collapse 状态以更新按钮文本
    try {
        const collapseEl = document.getElementById(`advancedOverrideCollapse-${workerId}`);
        if (collapseEl) {
            collapseEl.addEventListener('shown.bs.collapse', function() {
                const btn = configArea.querySelector('.toggle-advanced-override');
                if (btn) btn.textContent = '隐藏高级';
                if (btn) btn.setAttribute('aria-expanded', 'true');
            });
            collapseEl.addEventListener('hidden.bs.collapse', function() {
                const btn = configArea.querySelector('.toggle-advanced-override');
                if (btn) btn.textContent = '高级';
                if (btn) btn.setAttribute('aria-expanded', 'false');
            });
        }
    } catch (e) {}

    // 输入变化实时缓存
    try {
        const fi = configArea.querySelector('.config-file-input');
        if (fi) fi.addEventListener('input', () => persistWorkerConfig(workerId));
    } catch(e) {}
    // 拖拽逻辑保留但此处可能冗余（可后续移除）
    enableConfigOverridesDrag();
}

// 折叠/移除除当前 workerId 外的所有配置覆盖卡片（保持界面紧凑）
function collapseOtherWorkerConfigs(activeWorkerId) {
    const nodes = Array.from(document.querySelectorAll('.config-override-card'));
    nodes.forEach(n => {
        if (n.getAttribute('data-worker-id') !== activeWorkerId) {
            // 直接隐藏（用户可通过重新勾选再出现）
            n.style.display = 'none';
        }
    });
}

// 移除指定Worker的配置覆盖区域
function removeTaskConfigOverrideArea(workerId) {
    const configArea = document.querySelector(`#configOverride-${workerId}`);
    if (configArea) {
        try {
            // 平滑淡出再移除
            configArea.classList.add('fade-out');
            setTimeout(() => {
                if (configArea && configArea.parentNode) configArea.parentNode.removeChild(configArea);
            }, 220);
        } catch (e) {
            // fallback
            configArea.remove();
        }
    }
}

function showWorkerConfig(workerId) {
    const area = document.querySelector(`#configOverride-${workerId}`);
    if (area) {
        area.style.display = 'block';
        collapseOtherWorkerConfigs(workerId);
    } else {
        addTaskConfigOverrideArea(workerId);
    }
}

function persistWorkerConfig(workerId) {
    const area = document.querySelector(`#configOverride-${workerId}`);
    if (!area) return;
    try {
        const fi = area.querySelector('.config-file-input');
        window.__xfio_worker_config_cache[workerId] = {
            filename: fi ? fi.value : ''
        };
    } catch(e) {}
}

// 启用配置覆盖区域的拖拽重排
function enableConfigOverridesDrag() {
    const container = document.getElementById('taskConfigOverrides');
    if (!container) return;

    // attach handlers to each card (idempotent)
    container.querySelectorAll('.config-override-card').forEach(card => {
        if (card.__drag_init) return; // already initialized
        card.__drag_init = true;
        card.draggable = true;
        card.addEventListener('dragstart', function(e) {
            try { e.dataTransfer.setData('text/plain', card.id); } catch (err) {}
            card.classList.add('dragging');
            // set some visual hint
            card.style.opacity = '0.9';
        });

        card.addEventListener('dragend', function(e) {
            card.classList.remove('dragging');
            card.style.opacity = '';
        });

        card.addEventListener('dragover', function(e) {
            e.preventDefault();
            const dragging = container.querySelector('.dragging');
            if (!dragging || dragging === card) return;
            const rect = card.getBoundingClientRect();
            const midY = rect.top + rect.height / 2;
            if (e.clientY < midY) {
                container.insertBefore(dragging, card);
            } else {
                container.insertBefore(dragging, card.nextSibling);
            }
        });
    });
}

// 绑定复制按钮（多行 / 单行）以便复制当前预览内容
document.addEventListener('click', function initFioCopyButtons(e){
    // idempotent: if already bound, listener will early return
    const multiBtn = document.getElementById('copyFioCmdBtn');
    if (multiBtn && !multiBtn.__bound) {
        multiBtn.__bound = true;
        multiBtn.addEventListener('click', function(){
            try {
                const txt = document.getElementById('fioCmdPreview')?.textContent || '';
                navigator.clipboard.writeText(txt).then(()=>showNotification('已复制多行命令', 'success'));
            } catch(e){ showNotification('复制失败', 'error'); }
        });
    }
    // 单行复制已移除：保留多行复制（复制为展现的多行内容）
    // remove this initializer after first run
    document.removeEventListener('click', initFioCopyButtons);
});

// 创建任务
function assignTask() {
    // 防重复提交锁
    if (window.__assign_in_progress) return;
    window.__assign_in_progress = true;
    const submitBtn = document.getElementById('submitTaskBtn');
    const originalBtnHtml = submitBtn ? submitBtn.innerHTML : null;

    // 1. 校验任务名称
    const taskName = document.getElementById('taskNameInput').value.trim();
    if (!taskName) { showNotification('请输入任务名称', 'error'); return; }

    // 2. 校验用例
    const testCaseId = document.getElementById('testCaseSelect').value;
    if (!testCaseId) { showNotification('请选择用例', 'error'); return; }

    // 2. 收集选中 worker（按 DOM 顺序确保与预览一致）
    const selectedCheckboxes = Array.from(document.querySelectorAll('#workerSelectionArea .worker-card .worker-checkbox:checked'));
    if (!selectedCheckboxes.length) { showNotification('请至少选择一个 Worker 节点', 'error'); return; }

    // 3. 读取全局与覆盖状态
    const st = window.XFIO_ASSIGN_STATE || { global: { filename: '' }, overrides: {} };
    const globalPath = (st.global && st.global.filename ? st.global.filename.trim() : '');

    // 4. 构建 workers 数组（仅包含路径覆盖）
    const workersToAssign = [];
    let missingPathWorkers = [];
    selectedCheckboxes.forEach((cb, idx) => {
        const wid = cb.value;
        const ov = st.overrides[wid] || {};
        const filename = (ov.filename || '').trim() || globalPath.trim();
        if (!filename) missingPathWorkers.push(wid);
        const overrideObj = filename ? { filename, _sequence: idx + 1 } : { _sequence: idx + 1 };
        workersToAssign.push({ worker_id: wid, task_config_override: filename ? overrideObj : null });
    });

    if (missingPathWorkers.length) {
        showNotification(`存在 ${missingPathWorkers.length} 个节点没有测试路径（请设置全局路径或单独路径）`, 'error');
        return;
    }

    // 5. UI: 禁用按钮
    if (submitBtn) {
        submitBtn.disabled = true; submitBtn.setAttribute('aria-busy','true');
        submitBtn.innerHTML = '<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> 创建中...';
    }

    // Build assignment payload. Always include both workers (new format) & worker_ids (old format) for max compatibility.
    let assignment = { 
        name: taskName,
        test_case_id: testCaseId, 
        workers: workersToAssign.slice(),
        worker_ids: workersToAssign.map(w => w.worker_id)
    };

    try {
        const plan = (window.XFIO_EXEC_PLAN && Array.isArray(window.XFIO_EXEC_PLAN.steps) && window.XFIO_EXEC_PLAN.steps.length > 0) ? window.XFIO_EXEC_PLAN.steps : null;
            if (plan && plan.length > 1) {
            // build sanitized execution_plan: only keep necessary fields and per-step overrides
            const sanitized = plan.map((s, idx) => {
                // determine workers for this step: by default use selected workers
                const stepWorkers = selectedCheckboxes.map(cb => cb.value);
                // per-step overrides: include filename if provided in step.cfg.filename or global/override mapping
                const stepCfg = s.cfg || {};
                // sanitize params: remove size-based termination and enforce time_based/runtime/direct
                const sizeKeys = ['size','filesize','size_mb','filesize_mb','file_size','blocksize_bytes'];
                const params = {};
                // copy known params from cfg
                ['runtime','direct','bs','iodepth','numjobs','rw','ioengine'].forEach(k => {
                    if (stepCfg[k] !== undefined) params[k] = stepCfg[k];
                });
                // remove size keys
                sizeKeys.forEach(k => { if (stepCfg[k] !== undefined) { /* omit */ } });
                // enforce defaults
                if (params.runtime === undefined || params.runtime === null) params.runtime = 180;
                if (params.direct === undefined || params.direct === null) params.direct = 1;
                params.time_based = 1;
                // normalize booleans to 1/0
                Object.keys(params).forEach(k => { if (typeof params[k] === 'boolean') params[k] = params[k] ? 1 : 0; });

                // filename resolution: prefer per-step cfg.filename, else global state mapping
                let filename = '';
                if (stepCfg.filename) filename = stepCfg.filename;
                else {
                    // try from XFIO_ASSIGN_STATE overrides or global
                    const st = window.XFIO_ASSIGN_STATE || { global:{}, overrides:{} };
                    const g = st.global && st.global.filename ? st.global.filename.trim() : '';
                    filename = g;
                }

                return {
                    step: s.step || (idx+1),
                    test_case_id: s.test_case_id,
                    name: s.name || '',
                    workers: stepWorkers,
                    overrides: filename ? { filename: filename } : null,
                    params: params,
                    run_mode: s.run_mode || 'auto',
                    delay_after: s.delay_after || 0
                };
            });
                // include top-level test_case_id and workers array so backend accepts the payload
                assignment = { 
                    name: taskName,
                    test_case_id: testCaseId, 
                    workers: workersToAssign.slice(),
                    worker_ids: workersToAssign.map(w => w.worker_id),
                    plan_name: `plan-${Date.now()}`, 
                    execution_plan: sanitized
                };
        }
    } catch(e) { console.error('error building execution_plan', e); }

    // Fallback: if somehow workers became empty but we have execution_plan with step workers, reconstruct
    if ((!assignment.workers || !assignment.workers.length) && Array.isArray(assignment.execution_plan)) {
        const stepWorkers = new Set();
        assignment.execution_plan.forEach(st => { if (Array.isArray(st.workers)) st.workers.forEach(w => stepWorkers.add(w)); });
        if (stepWorkers.size) {
            assignment.workers = Array.from(stepWorkers).map((wid, idx) => ({ worker_id: wid, task_config_override: null }));
            assignment.worker_ids = Array.from(stepWorkers);
        }
    }

    // Debug: ensure we are sending workers and test_case_id
    try { console.debug('assignTask: payload', assignment); } catch(e){}

    if (!assignment.workers || !assignment.workers.length) {
        // defensive: should not happen because we validated selectedCheckboxes above
        showNotification('创建任务失败：未选择任何 Worker 节点（请返回第2步选择节点）', 'error');
        if (submitBtn) { submitBtn.disabled = false; submitBtn.removeAttribute('aria-busy'); submitBtn.innerHTML = originalBtnHtml; }
        return;
    }

    fetch('/api/tasks/assign', {
        method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(assignment)
    })
        .then(async resp => { const txt = await resp.text(); let data; try { data = txt ? JSON.parse(txt) : {}; } catch(e){ throw new Error(`服务器返回非JSON响应: ${txt}`);} if(!resp.ok) throw new Error(data.error||`HTTP ${resp.status}`); return data; })
        .then(data => {
            if (data.status === 'assigned') {
                showNotification(`成功创建了 ${data.task_ids.length} 个任务`, 'success');
                // 关闭模态
                const modalElement = document.getElementById('createTaskModal');
                try { let modal = null; if (typeof bootstrap !== 'undefined' && bootstrap.Modal) { modal = bootstrap.Modal.getInstance(modalElement) || new bootstrap.Modal(modalElement); } if (modal) modal.hide(); } catch(e){}
                // 重置状态
                window.XFIO_ASSIGN_STATE = { global: { filename: '' }, overrides: {} };
                const form = document.getElementById('createTaskForm'); if (form) form.reset();
                const taskNameInput = document.getElementById('taskNameInput'); if (taskNameInput) taskNameInput.value = '';
                renderWorkerOverridesUI();
                loadTasks(); updateTaskStats();
            } else {
                showNotification('创建任务失败: ' + (data.error || '未知错误'), 'error');
            }
        })
        .catch(err => { console.error('Error creating task:', err); showNotification('创建任务时发生错误: ' + err.message, 'error'); })
    .finally(() => { if (submitBtn) { submitBtn.disabled = false; submitBtn.removeAttribute('aria-busy'); submitBtn.innerHTML = originalBtnHtml; } window.__assign_in_progress = false; });
}

// 显示通知
function showNotification(message, type = 'info') {
    // 创建一个临时的通知元素
    const notification = document.createElement('div');
    notification.className = `alert alert-${type === 'error' ? 'danger' : type} alert-dismissible fade show position-fixed`;
    notification.style.cssText = 'top: 20px; right: 20px; min-width: 300px; z-index: 9999;';
    notification.innerHTML = `
        ${message}
        <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
    `;
    
    // 添加到页面
    document.body.appendChild(notification);
    
    // 3秒后自动移除
    setTimeout(() => {
        if (notification.parentNode) {
            notification.remove();
        }
    }, 3000);
}